Hello, Milos Nikic, le mer. 21 janv. 2026 14:58:01 -0800, a ecrit: > 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.
Great :D It'll take me some time to get a look at it, people are welcome to provide feedback of course! Samuel > 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 > -- Samuel <T> csp.tar.gz: ascii text -+- #ens-mim - vive les browsers qui prennent des initiatives à la con -+-
