This patch adds the core implementation of the JBD2 journaling engine
in journal.c and journal.h, and updates the Makefile to include
them in the build.
The implementation provides a lightweight, crash-consistent journaling
layer compatible with standard JBD2 (Linux ext3/ext4).
Key features of this implementation:
- Transaction Lifecycle: Supports `journal_start_transaction`,
`journal_dirty_block`, and `journal_commit_transaction`.
- Ring Buffer Management: Handles logical-to-physical block mapping
and wraps around the journal file correctly.
- Performance: Uses `hurd_ihash` to map filesystem blocks to pending
journal buffers in O(1) time, preventing O(N) slowdowns during
heavy write loads.
- Safety: Implements correct write barriers (`store_sync`) in the
commit path to prevent 'split-brain' metadata states on power loss.
- Deadlock Prevention: Introduces `journal_sync_everything` (extern)
logic to allow the journal to force a global checkpoint when full,
without re-entering the commit loop.
This engine is currently self-contained and will be hooked into the
main inode writing path in the subsequent patch.
---
ext2fs/Makefile | 2 +-
ext2fs/journal.c | 841 +++++++++++++++++++++++++++++++++++++++++++++++
ext2fs/journal.h | 62 ++++
3 files changed, 904 insertions(+), 1 deletion(-)
create mode 100644 ext2fs/journal.c
create mode 100644 ext2fs/journal.h
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/journal.c b/ext2fs/journal.c
new file mode 100644
index 00000000..a9e9cc30
--- /dev/null
+++ b/ext2fs/journal.c
@@ -0,0 +1,841 @@
+/* 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 <hurd/ihash.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;
+ struct hurd_ihash t_buffer_map; /* The Map (for O(1) lookups) */
+
+ /* 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
+flush_to_disk (void)
+{
+ error_t err = store_sync (store);
+ /* Ignore EOPNOTSUPP (drivers), but warn on real I/O errors */
+ if (err && err != EOPNOTSUPP)
+ ext2_warning ("device flush failed: %s", strerror (err));
+}
+
+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...");
+
+ flush_to_disk ();
+ JRNL_LOG_DEBUG
+ ("[CHECKPOINT] Reset complete. Tail moved to %u. Free space restored.",
+ journal->j_tail);
+}
+
+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 = 0;
+ 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 so new writers start a NEW transaction */
+ journal->j_running_transaction = NULL;
+ txn->t_state = T_LOCKED;
+ 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;
+
+ /* Ensure we have space in the ring buffer */
+ 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 is full. Force a global sync to reclaim all space. */
+ journal_force_checkpoint (journal, txn->t_tid);
+ }
+
+ /* Reserve Descriptor Block */
+ descriptor_loc = journal_next_log_block (journal);
+
+ /* Reserve Data Blocks */
+ jb = txn->t_buffers;
+ uint32_t expected = journal_next_after (journal, descriptor_loc);
+
+ while (jb)
+ {
+ jb->jb_log_spot = journal_next_log_block (journal);
+ /* Sanity Check: The log should yield contiguous blocks (accounting for
wrap) */
+ 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 Block */
+ commit_loc = journal_next_log_block (journal);
+ pthread_mutex_unlock (&journal->j_state_lock);
+
+ 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)
+ {
+ /* Safety: Don't overflow the descriptor block */
+ if (tag_offset + sizeof (journal_block_tag_t) > block_size)
+ {
+ ext2_warning ("[COMMIT] Transaction too large for single descriptor!
Dropping.");
+ 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;
+
+ 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;
+ }
+
+ /* We MUST wait for data to hit the disk before writing the commit record. */
+ flush_to_disk ();
+
+ /* Write 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);
+
+ err = journal_write_block (journal, commit_loc, commit_buf);
+ free (commit_buf);
+ commit_buf = NULL;
+
+ flush_to_disk ();
+
+ /* UPDATE JOURNAL METADATA */
+ if (!err)
+ {
+ pthread_mutex_lock (&journal->j_state_lock);
+ uint32_t total_used = txn->t_nr_blocks + 2;
+
+ if (journal->j_free > total_used)
+ journal->j_free -= total_used;
+ else
+ journal->j_free = 0;
+ if (journal->j_tail == 0)
+ {
+ journal->j_tail = journal->j_first;
+ journal_update_superblock (journal, txn->t_tid, journal->j_first);
+ }
+ pthread_mutex_unlock (&journal->j_state_lock);
+ }
+
+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;
+ }
+
+ hurd_ihash_destroy (&txn->t_buffer_map);
+ 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
+ {
+ /* Note: We don't check j_free here. If the journal is full,
+ journal_commit_transaction will detect it and trigger
+ journal_force_checkpoint() before writing. */
+ txn = calloc (1, sizeof (struct journal_transaction));
+ if (!txn)
+ {
+ pthread_mutex_unlock (&journal->j_state_lock);
+ return ENOMEM;
+ }
+
+ hurd_ihash_init (&txn->t_buffer_map, HURD_IHASH_NO_LOCP);
+ 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;
+ error_t err;
+
+ 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;
+ }
+
+ /* FAST PATH using Hurd's libihash */
+ jb = (journal_buffer_t *) hurd_ihash_find (&txn->t_buffer_map,
(hurd_ihash_key_t) fs_blocknr);
+
+ if (jb)
+ {
+ memcpy (jb->jb_shadow_data, data, block_size);
+ pthread_mutex_unlock (&journal->j_state_lock);
+ return 0;
+ }
+
+ /* SLOW PATH: Allocate new buffer wrapper */
+ 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);
+
+ /* Insert it into Hash Map */
+ err = hurd_ihash_add (&txn->t_buffer_map, (hurd_ihash_key_t) fs_blocknr,
(hurd_ihash_value_t) new_jb);
+ if (err)
+ {
+ /* Hash table resize failed (ENOMEM). */
+ free (new_jb->jb_shadow_data);
+ free (new_jb);
+ pthread_mutex_unlock (&journal->j_state_lock);
+ return err;
+ }
+
+ /* Link into the Transaction List */
+ 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;
+ 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)
+ {
+ if (hurd_ihash_find (&txn->t_buffer_map, (hurd_ihash_key_t) blocknr))
+ {
+ is_active = 1;
+ }
+ }
+
+ 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..fa279464
--- /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 1 /* 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
--
2.52.0