Hello, Thanks for the explanations and for caring about performance :)
I have applied v2. Samuel Milos Nikic, le ven. 25 sept. 2026 21:51:18 -0700, a ecrit: > Hello again. > Here is v2! > > After doing some telemetry and rm -rf on a large object (linux kernel)... i > realized that due to excessive syncing on the journal path i basically defeat > the idea of batching, because i create thousands of micro transaction (set > sync, cuts the transaction to commit as soon as possible) that only commit 5-6 > blocks at a time. > Its a large performance degradation without much added in terms of durability. > Things are already recorded in one transaction, and that is pretty durable... > Here is a v2 that is the same in everything else, except syncing has been > removed to restore the performance to "near" previous state. > > Thanks, > Milos > > On Fri, Sep 25, 2026 at 10:30 AM Milos Nikic <[1][email protected]> wrote: > > Hello, > > > No, it's not an unclean shutdown. > > > Fixed. > > > Can't it work without a journal? > Is the orphan list not a separate feature flag? > > > In theory, yes, but architecturally, it violates the constraints of the > Mach pager. > > Without a journal, the pager flushes dirty blocks asynchronously. If it > flushes a modified orphan inode to disk before it flushes the updated list > pointer (i_dtime) of the preceding node, the linked list breaks. I spent > the last few days tracing this exact race condition. Fsck is much more > strict when walking the orphan list than just detecting a node with 0 > i_dtime. Without the strict chronological serialization barrier provided > by > the journal, a hard crash leaves the linked list corrupted, turning simple > "zero dtime" fsck warnings into severe "corrupted block" errors and > similar. Gating the orphan list behind the journal guarantees the > write-ordering needed to safely maintain the on-disk pointers. > > Is that really needed? We'd normally only ever call diskfs_orphan_add > when st_nlink got down to 0. > > > Unlinked nodes often times get their blocks smashed together by > ext2/pager, > one unlinked file pointer starts pointing to a completely different files > blocks. > This is visible on unjornaled, unorphanted ext2. One can do > sudo apt update && sudo apt upgrade && sudo halt > And one will occasionally see interesting things in some of the unlinked > files. Some pointers from one file point to other files etc. > This is something i was trying to iron out for the last few days, but > there > is no way around it, couple of these fields need to be set in the orphan > for a cleaner fsck. > Basically we are racing with the pager, and some of these fields need to > be > altered together.. > > > Also, linux' ext4 seems to be using orphans for truncated files too, > we > might want to do that too. > > > This is a great idea, and i think its a great next patch :) > > > How is synchronization between the superblock, the inode, and the > journal handled? This needs to be explained in the comments. > > > Done > > > Can't we record that somewhere in memory? It can very quickly grow on > machine upgrade. > > > Done, now its a doubly linked list in-memory, so add/delete are constant > operations. > > > You can now drop the XXX: you are fixing that case. > > > Done. > > > There are other st_nlink-- in this file > > > Thanks, i think i got them all now. > > Btw the patch also reverts a "workaround" on diskfs_shutdown_pager > introduced in the previous journaling patch, its not needed anymore. > > Thanks, > Milos > > On Sat, Sep 19, 2026 at 10:54 AM Samuel Thibault <[2] > [email protected]> wrote: > > Hello, > > Milos Nikic, le mer. 16 sept. 2026 04:47:31 -0700, a ecrit: > > diff --git a/ext2fs/ext2fs.c b/ext2fs/ext2fs.c > > index 984df0448..5b996370e 100644 > > --- a/ext2fs/ext2fs.c > > +++ b/ext2fs/ext2fs.c > > @@ -271,6 +271,8 @@ main (int argc, char **argv) > > fprintf (stderr, "ext2fs: journaling enabled on %s\n", > diskfs_disk_name); > > JRNL_LOG_DEBUG ("Global Journal Initialized at %p", > ext2_journal); > > diskfs_nput(jnode); > > + /* Recover any orphan inodes left from a previous unclean > shutdown. */ > > No, it's not an unclean shutdown. > > Anything that was still memory-mapped but unlinked when ext2fs got > shut > down will be in that state. That's very common after upgrading daemons > or libraries. > > > > + ext2_recover_orphan_list (); > > > > diff --git a/ext2fs/orphan.c b/ext2fs/orphan.c > > new file mode 100644 > > index 000000000..bdd0eb90b > > --- /dev/null > > +++ b/ext2fs/orphan.c > > +/* Add inode NP to the orphan list. */ > > +void > > +diskfs_orphan_add (struct node *np) > > +{ > > + ino_t inum = np->cache_id; > > + struct ext2_inode *di; > > + diskfs_transaction_t *txn = NULL; > > + > > + if (!ext2_journal) > > + return; > > Can't it work without a journal? > > Is the orphan list not a separate feature flag? > > > + di->i_links_count = 0; > > Is that really needed? We'd normally only ever call diskfs_orphan_add > when st_nlink got down to 0. > > Also, linux' ext4 seems to be using orphans for truncated files too, > we > might want to do that too. > > > + /* Update the superblock to point to this inode as the new list > head. */ > > + sblock->s_last_orphan = htole32 (inum); > > + sblock_dirty = 1; > > + > > + if (txn) > > + { > > + journal_dirty_block (txn, boffs_block (bptr_offs (di))); > > + > > + /* Sync our private sblock to the Mach disk cache so the > journal captures it */ > > + memcpy (boffs_ptr (SBLOCK_OFFS), sblock, SBLOCK_SIZE); > > + journal_dirty_block (txn, boffs_block (SBLOCK_OFFS)); > > + } > > + > > + dino_deref (di); > > + > > + diskfs_node_disknode (np)->on_orphan_list = 1; > > + pthread_mutex_unlock (&orphan_lock); > > + > > + if (txn) > > + diskfs_journal_stop_transaction (txn); > > + else > > + alloc_sync (np); > > How is synchronization between the superblock, the inode, and the > journal handled? This needs to be explained in the comments. > > > +/* Remove inode NP from the orphan list. */ > > +void > > +diskfs_orphan_del (struct node *np) > > +{ > [...] > > + else > > + { > > + /* Walk the list to find the predecessor. */ > > Can't we record that somewhere in memory? It can very quickly grow on > machine upgrade. > > > +/* Recover (clean up) the orphan list at mount time. */ > > +void > > +ext2_recover_orphan_list (void) > > +{ > > + if (diskfs_readonly) > > + { > > + ext2_warning ("orphan inodes on readonly fs; leaving for > fsck"); > > + return; > > Please put this warning after checking for empty orphan list. > > > diff --git a/libdiskfs/dir-rename.c b/libdiskfs/dir-rename.c > > index 939d0b6ae..e85ef45bb 100644 > > --- a/libdiskfs/dir-rename.c > > +++ b/libdiskfs/dir-rename.c > > @@ -240,6 +240,8 @@ diskfs_S_dir_rename (struct protid *fromcred, > > diskfs_node_update (fdp, diskfs_synchronous); > > > > fnp->dn_stat.st_nlink--; > > + if (fnp->dn_stat.st_nlink == 0) > > + diskfs_orphan_add (fnp); > > fnp->dn_set_ctime = 1; > > > > diskfs_node_update (fnp, diskfs_synchronous); > > There are other st_nlink-- in this file, don't we want to orphan them? > > > diff --git a/libdiskfs/dir-renamed.c b/libdiskfs/dir-renamed.c > > index 97487ce53..8fcaffdba 100644 > > --- a/libdiskfs/dir-renamed.c > > +++ b/libdiskfs/dir-renamed.c > > @@ -212,6 +212,8 @@ diskfs_rename_dir (struct node *fdp, struct node > *fnp, const char *fromname, > > if (!err) > > { > > tnp->dn_stat.st_nlink--; > > + if (tnp->dn_stat.st_nlink == 0) > > + diskfs_orphan_add (tnp); > > tnp->dn_set_ctime = 1; > > } > > diskfs_clear_directory (tnp, tdp, tocred); > > Same here, there is another one in an error case. > > > @@ -251,6 +253,8 @@ diskfs_rename_dir (struct node *fdp, struct node > *fnp, const char *fromname, > > diskfs_dirremove (fdp, fnp, fromname, ds); > > ds = 0; > > fnp->dn_stat.st_nlink--; > > + if (fnp->dn_stat.st_nlink == 0) > > + diskfs_orphan_add (fnp); > > fnp->dn_set_ctime = 1; > > diskfs_file_update (fdp, diskfs_synchronous); > > diskfs_node_update (fnp, diskfs_synchronous); > > > > > diff --git a/libdiskfs/node-drop.c b/libdiskfs/node-drop.c > > index a12c29ad0..bb2d38e26 100644 > > --- a/libdiskfs/node-drop.c > > +++ b/libdiskfs/node-drop.c > > @@ -43,7 +43,7 @@ diskfs_drop_node (struct node *np) > > /* XXX: if the filesystem is readonly, we cannot remove the files > with no link > > You can now drop the XXX: you are fixing that case. > > > but e.g. memory mapping still in memory. This notably happens > when > > upgrading packages without restarting the corresponding > processes. Fsck > > - will have to fix them. */ > > + will have to fix them or the orphan list, if implemented. */ > > if (np->dn_stat.st_nlink == 0 && !diskfs_readonly) > > { > > diskfs_check_readonly (); > > @@ -85,9 +85,13 @@ diskfs_drop_node (struct node *np) > > np->dn_stat.st_rdev = 0; > > np->dn_set_ctime = np->dn_set_atime = 1; > > diskfs_node_update (np, diskfs_synchronous); > > + diskfs_orphan_del (np); > > diskfs_free_node (np, savemode); > > } > > else > > + /* Here we don't remove the node from the orphan list > > + so that on the next restart file system has the > > + opportunity to deal with it before fsck. */ > > diskfs_node_update (np, diskfs_synchronous); > > > > fshelp_drop_transbox (&np->transbox); > > Thanks, > Samuel > > > References: > > [1] mailto:[email protected] > [2] mailto:[email protected] > From 803f9d30970c65f44f93e0cb669159f21a03a245 Mon Sep 17 00:00:00 2001 > From: Milos Nikic <[email protected]> > Date: Mon, 14 Sep 2026 13:57:39 -0700 > Subject: [PATCH] ext2fs: Add ext3 style orphan list > > When the file system is in read-only mode it is not possible to > remove inodes from disk. This causes issues that fsck needs to fix > later. > > The orphan list serves as a list of nodes that have nlink == 0 in memory > but didn't manage to get deleted from disk in time. > Such a list gives file system an opportunity to remove them on the > following restart prior to fsck run. > Even if the file system itself doesn't fix it, fsck understands this > implementation of the orphan list and will gracefully clean it up the first > time it sees it. > > It works by utilizing the superblock's s_last_orphan as the head of the > singly linked list on disk. If s_last_orphan is 0, the list is empty and there > are no orphans. Otherwise it points to the last element added to that > list. That element is the node that needs to be deleted, so by > convention its i_dtime is altered to point to the next element and so on. > In memory, it is represented as a doubly linked list for efficient updates. > > This patch also reverts changes to libdiskfs/pager.c done by the journal > patch 370c37aae. (they were a workaround for the fact that orphan list wasn't > yet implemented). > --- > ext2fs/Makefile | 2 +- > ext2fs/ext2fs.c | 5 + > ext2fs/ext2fs.h | 12 ++ > ext2fs/ialloc.c | 3 + > ext2fs/inode.c | 46 +++--- > ext2fs/orphan.c | 343 ++++++++++++++++++++++++++++++++++++++++ > ext2fs/pager.c | 71 +-------- > libdiskfs/Makefile | 2 +- > libdiskfs/dir-clear.c | 4 + > libdiskfs/dir-init.c | 6 + > libdiskfs/dir-link.c | 4 + > libdiskfs/dir-rename.c | 11 +- > libdiskfs/dir-renamed.c | 13 ++ > libdiskfs/dir-rmdir.c | 2 + > libdiskfs/dir-unlink.c | 2 + > libdiskfs/diskfs.h | 14 ++ > libdiskfs/node-create.c | 1 + > libdiskfs/node-drop.c | 8 +- > libdiskfs/orphan.c | 40 +++++ > 19 files changed, 496 insertions(+), 93 deletions(-) > create mode 100644 ext2fs/orphan.c > create mode 100644 libdiskfs/orphan.c > > diff --git a/ext2fs/Makefile b/ext2fs/Makefile > index a2b0f1eef..3a8f1ada0 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 journal.c > + xattr.c journal.c orphan.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/ext2fs.c b/ext2fs/ext2fs.c > index 984df0448..9bdaa2b0a 100644 > --- a/ext2fs/ext2fs.c > +++ b/ext2fs/ext2fs.c > @@ -278,6 +278,11 @@ main (int argc, char **argv) > JRNL_LOG_DEBUG ("\n[JOURNAL CHECK] No Journal flag found."); > } > > + /* Recover unlinked but open inodes left from a previous shutdown. > + It won't run unless readonly flag is false. So not for the root > + filesystem and not for the unclean translator. */ > + ext2_recover_orphan_list (); > + > /* 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 0975457d1..5bf20e645 100644 > --- a/ext2fs/ext2fs.h > +++ b/ext2fs/ext2fs.h > @@ -179,6 +179,13 @@ struct disknode > partially allocated. */ > int last_page_partially_writable; > > + /* True if this inode is on the ext3 orphan list (nlink=0 but still > + open). The i_dtime field is used as the next pointer on disk. */ > + int on_orphan_list; > + /* Prev and next pointers in an in-memory doubly linked list of orphans. */ > + struct node *orphan_prev; > + struct node *orphan_next; > + > /* Index to start a directory lookup at. */ > int dir_idx; > }; > @@ -346,6 +353,8 @@ error_t > journal_dirty_block (diskfs_transaction_t * txn, block_t fs_blocknr); > > void journal_notify_block_changed (block_t block); > + > +void ext2_orphan_drop_ram_link (struct node *np); > > /* ---------------------------------------------------------------- */ > /* Random stuff calculated from the super block. */ > @@ -490,6 +499,9 @@ _dino_deref (struct ext2_inode *inode) > > /* Write all active disknodes into the inode pager. */ > void write_all_disknodes (void); > + > +/* Recover (clean up) the orphan inode list at mount time. */ > +void ext2_recover_orphan_list (void); > > /* ---------------------------------------------------------------- */ > > diff --git a/ext2fs/ialloc.c b/ext2fs/ialloc.c > index 3cf7a2401..7be8a15b8 100644 > --- a/ext2fs/ialloc.c > +++ b/ext2fs/ialloc.c > @@ -343,6 +343,9 @@ diskfs_alloc_node (struct node *dir, mode_t mode, struct > node **node) > diskfs_node_disknode (np)->info.i_next_alloc_goal = 0; > diskfs_node_disknode (np)->info.i_prealloc_block = 0; > diskfs_node_disknode (np)->info.i_prealloc_count = 0; > + diskfs_node_disknode (np)->on_orphan_list = 0; > + diskfs_node_disknode (np)->orphan_prev = NULL; > + diskfs_node_disknode (np)->orphan_next = NULL; > /* diskfs_node_disknode (np)->info.i_new_inode */ > > /* > diff --git a/ext2fs/inode.c b/ext2fs/inode.c > index 5fa5165e6..1b6dae06b 100644 > --- a/ext2fs/inode.c > +++ b/ext2fs/inode.c > @@ -62,6 +62,9 @@ diskfs_user_make_node (struct node **npp, struct > lookup_context *ctx) > dn->dirents = 0; > dn->dir_idx = 0; > dn->pager = 0; > + dn->on_orphan_list = 0; > + dn->orphan_prev = NULL; > + dn->orphan_next = NULL; > pthread_rwlock_init (&dn->alloc_lock, NULL); > pokel_init (&dn->indir_pokel, diskfs_disk_pager, disk_cache); > > @@ -74,6 +77,7 @@ diskfs_user_make_node (struct node **npp, struct > lookup_context *ctx) > void > diskfs_node_norefs (struct node *np) > { > + ext2_orphan_drop_ram_link (np); > if (diskfs_node_disknode (np)->dirents) > free (diskfs_node_disknode (np)->dirents); > assert_backtrace (!diskfs_node_disknode (np)->pager); > @@ -488,27 +492,31 @@ write_node (struct node *np) > info->i_flags |= EXT2_IMMUTABLE_FL; > di->i_flags = htole32 (info->i_flags); > > - if (st->st_mode == 0) > - /* Set dtime non-zero to indicate a deleted file. > - We don't clear i_size, i_blocks, and i_translator in this case, > - to give "undeletion" utilities a chance. */ > - di->i_dtime = htole32 (di->i_mtime); > - else > + /* The i_dtime and other fields here are used by the orphan machinery > + so we don't need to touch them here if a node is an orphan. */ > + if (!diskfs_node_disknode (np)->on_orphan_list) > { > - di->i_dtime = htole32 (0); > - di->i_size = htole32 (st->st_size); > - if (sizeof (off_t) >= 8 && !S_ISDIR (st->st_mode)) > - /* 64bit file size */ > - di->i_size_high = htole32 (st->st_size >> 32); > - di->i_blocks = htole32 (st->st_blocks); > + if (st->st_mode == 0) > + /* Set dtime non-zero to indicate a deleted file. */ > + di->i_dtime = htole32 (di->i_mtime); > + else > + { > + /* We don't clear i_size, i_blocks, and i_translator if mode > is 0, > + to give "undeletion" utilities a chance. */ > + di->i_dtime = htole32 (0); > + di->i_size = htole32 (st->st_size); > + if (sizeof (off_t) >= 8 && !S_ISDIR (st->st_mode)) > + /* 64bit file size */ > + di->i_size_high = htole32 (st->st_size >> 32); > + di->i_blocks = htole32 (st->st_blocks); > + } > + > + if (S_ISCHR(st->st_mode) || S_ISBLK(st->st_mode)) > + di->i_block[0] = htole32 (st->st_rdev); > + else > + memcpy (di->i_block, diskfs_node_disknode (np)->info.i_data, > + EXT2_N_BLOCKS * sizeof di->i_block[0]); > } > - > - if (S_ISCHR(st->st_mode) || S_ISBLK(st->st_mode)) > - di->i_block[0] = htole32 (st->st_rdev); > - else > - memcpy (di->i_block, diskfs_node_disknode (np)->info.i_data, > - EXT2_N_BLOCKS * sizeof di->i_block[0]); > - > diskfs_end_catch_exception (); > np->dn_stat_dirty = 0; > > diff --git a/ext2fs/orphan.c b/ext2fs/orphan.c > new file mode 100644 > index 000000000..56782d39b > --- /dev/null > +++ b/ext2fs/orphan.c > @@ -0,0 +1,343 @@ > +/* Ext3-style Orphan Inode List implementation for ext2fs. > + When a file is unlinked (nlink=0) but still held open by a process, > + the inode is added to the orphan list (anchored at s_last_orphan in > + the superblock). Each orphaned inode uses its i_dtime field as a > + "next" pointer in the singly-linked list on disk. On mount, the list is > + traversed and each orphan is cleaned up (truncated and freed). > + At runtime, they are organized as a doubly linked list in memory. > + > + 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. > + > + 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., 59 Temple Place - Suite 330, Boston, MA 02111, USA. */ > + > +#include "ext2fs.h" > +#include "journal.h" > +#include "libdiskfs/diskfs.h" > +#include <pthread.h> > +#include <string.h> > + > +/* Dedicated mutex to protect the Ext3 orphan linked list and s_last_orphan. > */ > +static pthread_mutex_t orphan_lock = PTHREAD_MUTEX_INITIALIZER; > + > +/* The in-memory head of the orphan doubly linked list. */ > +static struct node *ram_orphan_head = NULL; > + > +/* Add inode NP to the orphan list. NP is locked by the caller. */ > +void > +diskfs_orphan_add (struct node *np) > +{ > + ino_t inum = np->cache_id; > + struct ext2_inode *di; > + diskfs_transaction_t *txn = NULL; > + > + assert_backtrace (!diskfs_readonly); > + assert_backtrace (np->dn_stat.st_nlink == 0); > + > + if (!ext2_journal) > + return; > + > + if (diskfs_node_disknode (np)->on_orphan_list) > + return; > + > + /* SYNCHRONIZATION OVERVIEW: > + 1. orphan_lock: Protects the in-memory doubly linked list > (ram_orphan_head). > + 2. global_lock: Protects the in-memory superblock modifications. > + 3. Journal Transaction (txn): Guarantees that the superblock pointer > and the > + inode pointer hit the physical disk as a single, atomic operation. */ > + txn = diskfs_journal_start_transaction (); > + > + pthread_mutex_lock (&orphan_lock); > + > + if (diskfs_node_disknode (np)->on_orphan_list) > + { > + pthread_mutex_unlock (&orphan_lock); > + if (txn) > + diskfs_journal_stop_transaction (txn); > + return; > + } > + > + /* write_node must see this before it copies info.i_data. While set: > + leave i_dtime alone, and do not copy i_data, i_size, or i_blocks. > + libdiskfs must call diskfs_node_update as soon as this function > returns. */ > + diskfs_node_disknode (np)->on_orphan_list = 1; > + > + ext2_debug ("adding inode %lu to orphan list", (unsigned long) inum); > + > + di = dino_ref (inum); > + > + /* Atomically link the new orphan to the head of the on-disk list. */ > + pthread_spin_lock (&global_lock); > + di->i_dtime = sblock->s_last_orphan; > + sblock->s_last_orphan = htole32 (inum); > + sblock_dirty = 1; > + pthread_spin_unlock (&global_lock); > + > + /* Isolate the inode from standard file system deletion logic. > + Zeroing the block map here prevents the Mach pager from flushing > garbage or > + cross-linked block pointers to the disk before the journal commits. */ > + di->i_links_count = 0; > + di->i_size = 0; > + di->i_blocks = 0; > + if (!S_ISDIR (np->dn_stat.st_mode)) > + di->i_size_high = 0; > + memset (di->i_block, 0, EXT2_N_BLOCKS * sizeof di->i_block[0]); > + > + if (txn) > + { > + /* Atomically bundle the superblock and the placeholder inode. > + By dirtying both blocks in the same transaction, we guarantee that a > + crash cannot leave a severed list chain. */ > + memcpy (boffs_ptr (SBLOCK_OFFS), sblock, SBLOCK_SIZE); > + journal_dirty_block (txn, boffs_block (bptr_offs (di))); > + journal_dirty_block (txn, boffs_block (SBLOCK_OFFS)); > + } > + > + dino_deref (di); > + > + /* Maintain the in-memory doubly linked list for O(1) removals. */ > + diskfs_node_disknode (np)->orphan_prev = NULL; > + diskfs_node_disknode (np)->orphan_next = ram_orphan_head; > + if (ram_orphan_head) > + diskfs_node_disknode (ram_orphan_head)->orphan_prev = np; > + ram_orphan_head = np; > + > + pthread_mutex_unlock (&orphan_lock); > + > + if (txn) > + diskfs_journal_stop_transaction (txn); > + else > + diskfs_set_hypermetadata (0, 0); > +} > + > +/* Remove inode NP from the orphan list. NP is locked by the caller. */ > +void > +diskfs_orphan_del (struct node *np) > +{ > + ino_t inum = np->cache_id; > + diskfs_transaction_t *txn = NULL; > + int update_super = 0; > + > + if (!ext2_journal) > + return; > + > + if (!diskfs_node_disknode (np)->on_orphan_list) > + return; > + > + txn = diskfs_journal_start_transaction (); > + > + pthread_mutex_lock (&orphan_lock); > + > + if (!diskfs_node_disknode (np)->on_orphan_list) > + { > + pthread_mutex_unlock (&orphan_lock); > + if (txn) > + diskfs_journal_stop_transaction (txn); > + return; > + } > + > + ext2_debug ("removing inode %lu from orphan list", (unsigned long) inum); > + > + struct ext2_inode *my_di = dino_ref (inum); > + __u32 my_next = le32toh (my_di->i_dtime); > + > + /* This inode is leaving the list. i_dtime becomes a normal deletion > + stamp in the caller's following write_node (mode is already 0). > + We do not journal_dirty it here: that copy can run before write_node > + stores the cleared block map. */ > + my_di->i_dtime = 0; > + dino_deref (my_di); > + > + struct node *prev = diskfs_node_disknode (np)->orphan_prev; > + struct node *next = diskfs_node_disknode (np)->orphan_next; > + > + if (prev == NULL) > + { > + pthread_spin_lock (&global_lock); > + sblock->s_last_orphan = htole32 (my_next); > + sblock_dirty = 1; > + pthread_spin_unlock (&global_lock); > + > + if (txn) > + { > + memcpy (boffs_ptr (SBLOCK_OFFS), sblock, SBLOCK_SIZE); > + journal_dirty_block (txn, boffs_block (SBLOCK_OFFS)); > + } > + else > + update_super = 1; > + > + ram_orphan_head = next; > + } > + else > + { > + struct ext2_inode *prev_di = dino_ref (prev->cache_id); > + > + /* prev stays on the list, so its cached i_block[] is already the > + placeholder (zeros). Only i_dtime changes. */ > + prev_di->i_dtime = htole32 (my_next); > + if (txn) > + journal_dirty_block (txn, boffs_block (bptr_offs (prev_di))); > + dino_deref (prev_di); > + > + diskfs_node_disknode (prev)->orphan_next = next; > + } > + > + if (next) > + diskfs_node_disknode (next)->orphan_prev = prev; > + > + diskfs_node_disknode (np)->on_orphan_list = 0; > + diskfs_node_disknode (np)->orphan_prev = NULL; > + diskfs_node_disknode (np)->orphan_next = NULL; > + > + pthread_mutex_unlock (&orphan_lock); > + > + if (update_super) > + diskfs_set_hypermetadata (0, 0); > + else > + diskfs_journal_stop_transaction (txn); > +} > + > +/* Recover (clean up) the orphan list at mount time. > + > + Only inodes with nlink == 0 are truncated and freed. Entries with > + nlink != 0 (corruption, or ext3-style truncate orphans, which we do > + not support yet) are just taken off the list and otherwise left alone. > + > + The on-disk list is untrusted: each inode number is range-checked and > + the walk is bounded by the total inode count, so a cycle in the chain > + cannot hang the mount. */ > +void > +ext2_recover_orphan_list (void) > +{ > + ino_t inum; > + __u32 next_orphan; > + __u32 steps = 0; > + int count = 0; > + struct ext2_inode *di; > + struct node *np = NULL; > + error_t err; > + __u32 max_inodes = le32toh (sblock->s_inodes_count); > + > + next_orphan = le32toh (sblock->s_last_orphan); > + > + if (next_orphan == 0) > + return; > + > + if (diskfs_readonly) > + { > + ext2_warning ("orphan inodes on readonly fs; leaving for fsck"); > + return; > + } > + > + /* No locks needed for the RAM list here; the filesystem is strictly > + single-threaded during mount. */ > + while (next_orphan != 0) > + { > + inum = next_orphan; > + > + /* Validate before touching the disk. Counting steps (not > + recoveries) also bounds the nlink != 0 path below. */ > + if (inum < EXT2_FIRST_INO (sblock) || inum > max_inodes > + || ++steps > max_inodes) > + { > + ext2_warning ("corrupt or cyclic orphan list (inode %lu); " > + "aborting recovery", (unsigned long) inum); > + /* Break the chain so we don't re-walk garbage on every mount. */ > + pthread_spin_lock (&global_lock); > + sblock->s_last_orphan = 0; > + sblock_dirty = 1; > + pthread_spin_unlock (&global_lock); > + break; > + } > + > + /* Read the next pointer before lookup/deletion: orphan_del will > + scrub this inode's i_dtime. */ > + di = dino_ref (inum); > + next_orphan = le32toh (di->i_dtime); > + dino_deref (di); > + > + err = diskfs_cached_lookup (inum, &np); > + if (err || !np) > + { > + ext2_warning ("cannot look up orphan inode %lu: %s", > + (unsigned long) inum, > + err ? strerror (err) : "not found"); > + /* We can't safely unlink an inode we can't load. Leave the > + rest of the list, including s_last_orphan, for fsck. */ > + break; > + } > + > + /* Mock the RAM list state so that diskfs_orphan_del can natively > + update the disk structure and s_last_orphan. This inode is > + always the current head, so prev == NULL. */ > + diskfs_node_disknode (np)->on_orphan_list = 1; > + diskfs_node_disknode (np)->orphan_prev = NULL; > + diskfs_node_disknode (np)->orphan_next = NULL; > + ram_orphan_head = np; > + > + if (np->dn_stat.st_nlink != 0) > + { > + /* Not a deleted file, so diskfs_nput would not drop it and > + orphan_del would never run. Unlink it explicitly; this > + scrubs i_dtime, advances s_last_orphan and clears the head. */ > + ext2_warning ("orphan inode %lu has nlink > 0; unlinking from list", > + (unsigned long) inum); > + diskfs_orphan_del (np); > + diskfs_nput (np); > + continue; > + } > + > + /* This drops the reference. Since st_nlink == 0, libdiskfs will > + truncate the file and call diskfs_orphan_del (np), which > + advances s_last_orphan and clears the head. */ > + diskfs_nput (np); > + count++; > + } > + > + ram_orphan_head = NULL; > + > + if (count > 0) > + { > + /* Lets sync it, so that fsck doesn't try to clear the same orphans > again. */ > + diskfs_set_hypermetadata (1, 0); > + ext2_warning ("recovered %d orphan inode(s)", count); > + } > +} > + > +void > +ext2_orphan_drop_ram_link (struct node *np) > +{ > + pthread_mutex_lock (&orphan_lock); > + > + if (diskfs_node_disknode (np)->on_orphan_list) > + { > + struct node *prev = diskfs_node_disknode (np)->orphan_prev; > + struct node *next = diskfs_node_disknode (np)->orphan_next; > + > + if (prev) > + diskfs_node_disknode (prev)->orphan_next = next; > + else > + ram_orphan_head = next; > + if (next) > + diskfs_node_disknode (next)->orphan_prev = prev; > + > + diskfs_node_disknode (np)->orphan_prev = NULL; > + diskfs_node_disknode (np)->orphan_next = NULL; > + } > + > + pthread_mutex_unlock (&orphan_lock); > +} > diff --git a/ext2fs/pager.c b/ext2fs/pager.c > index 73c7d0b3c..efc104022 100644 > --- a/ext2fs/pager.c > +++ b/ext2fs/pager.c > @@ -964,7 +964,7 @@ pager_report_extent (struct user_pager_info *pager, > void > pager_clear_user_data (struct user_pager_info *upi) > { > - if (upi->type == FILE_DATA && upi->node) > + if (upi->type == FILE_DATA) > { > struct pager *pager; > > @@ -1561,81 +1561,18 @@ diskfs_get_filemap_pager_struct (struct node *node) > void > diskfs_shutdown_pager (void) > { > - /* TODO: Implement the Ext3/Ext4 Orphan Inode List (s_last_orphan). > - Currently, if a file is unlinked (nlink=0) but still held open > by a > - Mach pager, it will be abandoned on disk without dtime=0 if the > - system halts, causing fsck to complain. This manual teardown > forces > - the nodes to drop synchronously before the final journal commit. > - Once the Orphan List is implemented, this entire manual pager > cleanup > - can be safely removed. Unlinked files will be added to the > superblock's > - orphan list, and the OS can just pull the power. The next boot > will > - silently clean them up. */ > - error_t shutdown_and_clear (void *v_p) > + error_t shutdown_one (void *v_p) > { > struct pager *p = v_p; > - struct user_pager_info *upi = pager_get_upi (p); > - > - /* First, shutdown the pager: sync and flush all dirty pages, > - then destroy the port right. This must happen before we > - release the node reference, because pager_sync/pager_flush > - may need to access the node's allocsize and alloc_lock. */ > pager_shutdown (p); > - > - /* After pager_shutdown, the pager has been removed from the > - bucket's hash table (via ports_destroy_right). But we can > - still access it because ports_bucket_iterate holds a hard > - reference on our behalf. > - > - Now release the pager's weak node reference, mimicking what > - pager_dropweak + pager_clear_user_data would do. This > - ensures diskfs_drop_node runs synchronously for any unlinked > - nodes before we commit the final journal transaction. > - > - Without this, unlinked nodes would be left in a half-deleted > - state: nlink=0 on disk but dtime unset, bitmap not cleared, > - and free-counts not updated — all in an uncommitted journal > - transaction lost on exit(0). */ > - if (upi->type == FILE_DATA && upi->node) > - { > - int cleared = 0; > - > - /* Clear the node->pager back-pointer (as pager_dropweak does) > - so the assert in pager_clear_user_data is satisfied. */ > - pthread_spin_lock (&node_to_page_lock); > - if (diskfs_node_disknode (upi->node)->pager > - && pager_get_upi (diskfs_node_disknode (upi->node)->pager) == > upi) > - { > - diskfs_node_disknode (upi->node)->pager = NULL; > - cleared = 1; > - } > - pthread_spin_unlock (&node_to_page_lock); > - > - if (cleared) > - ports_port_deref_weak (p); > - > - /* Release the weak node reference acquired in diskfs_get_filemap. > - If this is the last reference, diskfs_drop_node is called > - synchronously, which sets dtime, clears the inode bitmap, > - and updates free-counts. */ > - diskfs_nrele_light (upi->node); > - > - /* Prevent pager_clear_user_data (which fires when the iterator > - drops its hard ref) from double-releasing the node. */ > - upi->node = NULL; > - } > - > return 0; > } > > - ports_bucket_iterate (file_pager_bucket, shutdown_and_clear); > - > - /* pager_shutdown + diskfs_nrele_light above may have triggered > - diskfs_drop_node for unlinked nodes, which writes dtime, clears > - the inode bitmap, updates free-counts, and starts a new journal > - transaction. We MUST commit this transaction before quiescing. */ > write_all_disknodes (); > journal_commit_running_transaction (); > > + ports_bucket_iterate (file_pager_bucket, shutdown_one); > + > if (!ext2_journal) > { > error_t err = store_sync (store); > diff --git a/libdiskfs/Makefile b/libdiskfs/Makefile > index 2b5a4a3b9..341532466 100644 > --- a/libdiskfs/Makefile > +++ b/libdiskfs/Makefile > @@ -52,7 +52,7 @@ OTHERSRCS = conch-fetch.c conch-set.c dir-clear.c > dir-init.c dir-renamed.c \ > remount.c console.c disk-pager.c \ > name-cache.c direnter.c dirrewrite.c dirremove.c lookup.c dead-name.c \ > validate-mode.c validate-group.c validate-author.c validate-flags.c \ > - validate-rdev.c validate-owner.c priv.c get-source.c journal.c > + validate-rdev.c validate-owner.c priv.c get-source.c journal.c orphan.c > SRCS = $(OTHERSRCS) $(FSSRCS) $(IOSRCS) $(FSYSSRCS) $(IFSOCKSRCS) > installhdrs = diskfs.h diskfs-pager.h > > diff --git a/libdiskfs/dir-clear.c b/libdiskfs/dir-clear.c > index d61baf98d..67e3eaad9 100644 > --- a/libdiskfs/dir-clear.c > +++ b/libdiskfs/dir-clear.c > @@ -48,6 +48,8 @@ diskfs_clear_directory (struct node *dp, > > /* Decrement the link count */ > dp->dn_stat.st_nlink--; > + if (dp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (dp); > dp->dn_set_ctime = 1; > > /* Find and remove the `..' entry. */ > @@ -65,6 +67,8 @@ diskfs_clear_directory (struct node *dp, > > /* Decrement the link count on the parent */ > pdp->dn_stat.st_nlink--; > + if (pdp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (pdp); > pdp->dn_set_ctime = 1; > > diskfs_truncate (dp, 0); > diff --git a/libdiskfs/dir-init.c b/libdiskfs/dir-init.c > index 5b1fb8373..8a1f14383 100644 > --- a/libdiskfs/dir-init.c > +++ b/libdiskfs/dir-init.c > @@ -51,6 +51,8 @@ diskfs_init_dir (struct node *dp, struct node *pdp, struct > protid *cred) > if (err) > { > dp->dn_stat.st_nlink--; > + if (dp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (dp); > dp->dn_set_ctime = 1; > diskfs_node_update (dp, diskfs_synchronous); > > @@ -67,10 +69,14 @@ diskfs_init_dir (struct node *dp, struct node *pdp, > struct protid *cred) > { > /* ROLLBACK '.' on Parent */ > pdp->dn_stat.st_nlink--; > + if (pdp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (pdp); > pdp->dn_set_ctime = 1; > diskfs_node_update (pdp, diskfs_synchronous); > /* CLEANUP '.' on Child */ > dp->dn_stat.st_nlink--; > + if (dp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (dp); > dp->dn_set_ctime = 1; > diskfs_node_update (dp, diskfs_synchronous); > return err; > diff --git a/libdiskfs/dir-link.c b/libdiskfs/dir-link.c > index ec3c0a3d8..608ff2183 100644 > --- a/libdiskfs/dir-link.c > +++ b/libdiskfs/dir-link.c > @@ -121,6 +121,8 @@ diskfs_S_dir_link (struct protid *dircred, > { > /* Deallocate link on TNP */ > tnp->dn_stat.st_nlink--; > + if (tnp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (tnp); > tnp->dn_set_ctime = 1; > diskfs_node_update (tnp, diskfs_synchronous); > } > @@ -132,6 +134,8 @@ diskfs_S_dir_link (struct protid *dircred, > if (err) > { > np->dn_stat.st_nlink--; > + if (np->dn_stat.st_nlink == 0) > + diskfs_orphan_add (np); > np->dn_set_ctime = 1; > diskfs_node_update (np, diskfs_synchronous); > } > diff --git a/libdiskfs/dir-rename.c b/libdiskfs/dir-rename.c > index 939d0b6ae..6328351ad 100644 > --- a/libdiskfs/dir-rename.c > +++ b/libdiskfs/dir-rename.c > @@ -182,6 +182,8 @@ diskfs_S_dir_rename (struct protid *fromcred, > if (!err) > { > tnp->dn_stat.st_nlink--; > + if (tnp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (tnp); > tnp->dn_set_ctime = 1; > diskfs_node_update (tnp, diskfs_synchronous); > } > @@ -197,7 +199,12 @@ diskfs_S_dir_rename (struct protid *fromcred, > if (err) > { > if (fnp->dn_stat.st_nlink > 0) > - fnp->dn_stat.st_nlink--; > + { > + fnp->dn_stat.st_nlink--; > + if (fnp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (fnp); > + } > + > fnp->dn_set_ctime = 1; > diskfs_node_update (fnp, diskfs_synchronous); > pthread_mutex_unlock (&fnp->lock); > @@ -240,6 +247,8 @@ diskfs_S_dir_rename (struct protid *fromcred, > diskfs_node_update (fdp, diskfs_synchronous); > > fnp->dn_stat.st_nlink--; > + if (fnp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (fnp); > fnp->dn_set_ctime = 1; > > diskfs_node_update (fnp, diskfs_synchronous); > diff --git a/libdiskfs/dir-renamed.c b/libdiskfs/dir-renamed.c > index 97487ce53..67174874d 100644 > --- a/libdiskfs/dir-renamed.c > +++ b/libdiskfs/dir-renamed.c > @@ -164,6 +164,8 @@ diskfs_rename_dir (struct node *fdp, struct node *fnp, > const char *fromname, > { > assert_backtrace (tdp->dn_stat.st_nlink > 0); > tdp->dn_stat.st_nlink--; > + if (tdp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (tdp); > tdp->dn_set_ctime = 1; > diskfs_node_update (tdp, diskfs_synchronous); > diskfs_drop_dirstat (fnp, tmpds); > @@ -177,6 +179,8 @@ diskfs_rename_dir (struct node *fdp, struct node *fnp, > const char *fromname, > { > assert_backtrace (tdp->dn_stat.st_nlink > 0); > tdp->dn_stat.st_nlink--; > + if (tdp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (tdp); > tdp->dn_set_ctime = 1; > diskfs_node_update (tdp, diskfs_synchronous); > > @@ -184,6 +188,9 @@ diskfs_rename_dir (struct node *fdp, struct node *fnp, > const char *fromname, > } > > fdp->dn_stat.st_nlink--; > + if (fdp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (fdp); > + > fdp->dn_set_ctime = 1; > diskfs_node_update (fdp, diskfs_synchronous); > } > @@ -212,6 +219,8 @@ diskfs_rename_dir (struct node *fdp, struct node *fnp, > const char *fromname, > if (!err) > { > tnp->dn_stat.st_nlink--; > + if (tnp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (tnp); > tnp->dn_set_ctime = 1; > } > diskfs_clear_directory (tnp, tdp, tocred); > @@ -227,6 +236,8 @@ diskfs_rename_dir (struct node *fdp, struct node *fnp, > const char *fromname, > { > assert_backtrace (fnp->dn_stat.st_nlink > 0); > fnp->dn_stat.st_nlink--; > + if (fnp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (fnp); > fnp->dn_set_ctime = 1; > /* fnp is locked, so this is safe */ > diskfs_node_update (fnp, diskfs_synchronous); > @@ -251,6 +262,8 @@ diskfs_rename_dir (struct node *fdp, struct node *fnp, > const char *fromname, > diskfs_dirremove (fdp, fnp, fromname, ds); > ds = 0; > fnp->dn_stat.st_nlink--; > + if (fnp->dn_stat.st_nlink == 0) > + diskfs_orphan_add (fnp); > fnp->dn_set_ctime = 1; > diskfs_file_update (fdp, diskfs_synchronous); > diskfs_node_update (fnp, diskfs_synchronous); > diff --git a/libdiskfs/dir-rmdir.c b/libdiskfs/dir-rmdir.c > index de288aae1..82c20a7ea 100644 > --- a/libdiskfs/dir-rmdir.c > +++ b/libdiskfs/dir-rmdir.c > @@ -90,6 +90,8 @@ diskfs_S_dir_rmdir (struct protid *dircred, > if (!error) > { > np->dn_stat.st_nlink--; > + if (np->dn_stat.st_nlink == 0) > + diskfs_orphan_add (np); > np->dn_set_ctime = 1; > diskfs_clear_directory (np, dnp, dircred); > diskfs_file_update (np, diskfs_synchronous); > diff --git a/libdiskfs/dir-unlink.c b/libdiskfs/dir-unlink.c > index 4ceaec4b9..85df1c977 100644 > --- a/libdiskfs/dir-unlink.c > +++ b/libdiskfs/dir-unlink.c > @@ -79,6 +79,8 @@ diskfs_S_dir_unlink (struct protid *dircred, > > np->dn_stat.st_nlink--; > np->dn_set_ctime = 1; > + if (np->dn_stat.st_nlink == 0) > + diskfs_orphan_add (np); > diskfs_node_update (np, diskfs_synchronous); > > if (np->dn_stat.st_nlink == 0) > diff --git a/libdiskfs/diskfs.h b/libdiskfs/diskfs.h > index d8dac1293..7c86cf324 100644 > --- a/libdiskfs/diskfs.h > +++ b/libdiskfs/diskfs.h > @@ -584,6 +584,20 @@ int diskfs_journal_needs_sync (diskfs_transaction_t > *txn); > The default definition does nothing. */ > void diskfs_journal_shutdown (void); > > +/* Orphan Inode List hooks. > + These are called by libdiskfs when a file is unlinked (nlink drops to > + 0) but still held open, and when the inode is finally freed. > + Filesystems with an ext3-style Orphan List (e.g. ext2fs) should > + override the weak default implementations. */ > + > +/* Add inode NP to the orphan list. Called when nlink drops to 0 while > + the node is still held open (has hard references). NP must be locked. */ > +void diskfs_orphan_add (struct node *np); > + > +/* Remove inode NP from the orphan list. Called when the inode is about > + to be permanently freed in diskfs_drop_node. NP must be locked. */ > +void diskfs_orphan_del (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-create.c b/libdiskfs/node-create.c > index 3f30cde6d..28d2c6e34 100644 > --- a/libdiskfs/node-create.c > +++ b/libdiskfs/node-create.c > @@ -154,6 +154,7 @@ diskfs_create_node (struct node *dir, > diskfs_clear_directory (np, dir, cred); > np->dn_stat.st_nlink = 0; > np->dn_set_ctime = 1; > + diskfs_orphan_add (np); > diskfs_node_update (np, diskfs_synchronous); > diskfs_nput (np); > } > diff --git a/libdiskfs/node-drop.c b/libdiskfs/node-drop.c > index a12c29ad0..c1c6cee2f 100644 > --- a/libdiskfs/node-drop.c > +++ b/libdiskfs/node-drop.c > @@ -40,10 +40,6 @@ diskfs_drop_node (struct node *np) > mode_t savemode; > diskfs_transaction_t *txn = diskfs_journal_start_transaction (); > > - /* XXX: if the filesystem is readonly, we cannot remove the files with no > link > - but e.g. memory mapping still in memory. This notably happens when > - upgrading packages without restarting the corresponding processes. Fsck > - will have to fix them. */ > if (np->dn_stat.st_nlink == 0 && !diskfs_readonly) > { > diskfs_check_readonly (); > @@ -84,10 +80,14 @@ diskfs_drop_node (struct node *np) > np->dn_stat.st_mode = 0; > np->dn_stat.st_rdev = 0; > np->dn_set_ctime = np->dn_set_atime = 1; > + diskfs_orphan_del (np); > diskfs_node_update (np, diskfs_synchronous); > diskfs_free_node (np, savemode); > } > else > + /* Here we don't remove the node from the orphan list > + so that on the next restart, the file system has the > + opportunity to deal with it before fsck. */ > diskfs_node_update (np, diskfs_synchronous); > > fshelp_drop_transbox (&np->transbox); > diff --git a/libdiskfs/orphan.c b/libdiskfs/orphan.c > new file mode 100644 > index 000000000..fed512de3 > --- /dev/null > +++ b/libdiskfs/orphan.c > @@ -0,0 +1,40 @@ > +/* Default orphan list hooks for libdiskfs. > + Provides weak no-op implementations of the orphan list functions. > + Filesystems with an ext3-style orphan list (e.g. ext2fs) override these. > + > + Written by Milos Nikic. > + Copyright (C) 2026 Free Software Foundation, Inc. > + > + 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. > + > + 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., 59 Temple Place - Suite 330, Boston, MA 02111, USA. */ > + > +#include "diskfs.h" > + > +/* Add inode NP to the orphan list. Called when nlink drops to 0 > + while the node is still held open (has hard references). > + NP must be locked. The default implementation does nothing. */ > +void __attribute__((weak)) diskfs_orphan_add (struct node *np) > +{ > + /* Do nothing */ > +} > + > +/* Remove inode NP from the orphan list. Called when the inode is > + about to be permanently freed in diskfs_drop_node. > + NP must be locked. The default implementation does nothing. */ > +void __attribute__((weak)) diskfs_orphan_del (struct node *np) > +{ > + /* Do nothing */ > +} > -- > 2.55.0 > -- Samuel RK > Some "PC speakers" are actually buzzers in some cases rather than RK > real loudspeakers which give a squark rather than a beep. DW They're not _that_ bad. Even on most recent hardware, mp3s played DW through the PC speaker are relatively recognisable :) -+- lkml -+-
