Hello, Yes e2fsck does it regardless of the check count, e2fsck checks the s_last_orphan field in the superblock before deciding to skip the scan. Even if the mount count hasn't been reached and the filesystem is marked clean, if s_last_orphan is non-zero, e2fsck -p will run the "Clearing orphaned inodes" routine before exiting.
And yes it is the filesystem's duty to process it on filesystem mount, and this patch already adds this behavior! In this patch, I added ext2_recover_orphan_list() function call directly into main() in ext2fs.c. So: - For settrans mounts (read-write), ext2fs starts up, sees the orphan list, and immediately sweeps it and clean itself. fsck is not needed. - For root drive (read-only Mount): During the initial boot sequence, the root filesystem initially mounts it read-only. ext2_recover_orphan_list detects this (if (diskfs_readonly)) and aborts, leaving the list intact for /sbin/init to clean up using e2fsck before remounting it read-write. Thank you, Milos On Wed, Sep 16, 2026 at 4:53 AM Samuel Thibault <[email protected]> wrote: > Hello, > > Thanks for working on it, that'll clean quite a few warnings that scare > people :) > > Milos Nikic, le mer. 16 sept. 2026 04:47:31 -0700, a ecrit: > > Even if file system itself doesn't fix it, fsck understands this > > implementation of orphan list and will gracefully clean it up first > > time it sees it. > > Does it do it even when the check count was not reached? > > Is it perhaps the duty of ext2fs to process it on filesystem mount? > > Samuel > > > It works by utilizing superblock's s_last_orphan as the head of the > > singly linked list. 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. > > --- > > ext2fs/Makefile | 2 +- > > ext2fs/ext2fs.c | 2 + > > ext2fs/ext2fs.h | 7 ++ > > ext2fs/inode.c | 22 ++-- > > ext2fs/orphan.c | 215 ++++++++++++++++++++++++++++++++++++++++ > > libdiskfs/Makefile | 2 +- > > libdiskfs/dir-link.c | 4 + > > libdiskfs/dir-rename.c | 2 + > > libdiskfs/dir-renamed.c | 4 + > > libdiskfs/dir-rmdir.c | 2 + > > libdiskfs/dir-unlink.c | 5 +- > > libdiskfs/diskfs.h | 14 +++ > > libdiskfs/node-drop.c | 6 +- > > libdiskfs/orphan.c | 40 ++++++++ > > 14 files changed, 316 insertions(+), 11 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..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. */ > > + ext2_recover_orphan_list (); > > } > > } > > else > > diff --git a/ext2fs/ext2fs.h b/ext2fs/ext2fs.h > > index 0975457d1..65946dd9e 100644 > > --- a/ext2fs/ext2fs.h > > +++ b/ext2fs/ext2fs.h > > @@ -179,6 +179,10 @@ 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. */ > > + int on_orphan_list; > > + > > /* Index to start a directory lookup at. */ > > int dir_idx; > > }; > > @@ -490,6 +494,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/inode.c b/ext2fs/inode.c > > index 5fa5165e6..46df08251 100644 > > --- a/ext2fs/inode.c > > +++ b/ext2fs/inode.c > > @@ -488,14 +488,22 @@ 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 field is actively used as a linked list pointer > > + by the orphan list machinery, which updates the disk cache > > + Do not overwrite it for orphans. */ > > + if (!diskfs_node_disknode (np)->on_orphan_list) > > + { > > + if (st->st_mode == 0) > > + /* Set dtime non-zero to indicate a deleted file. */ > > + di->i_dtime = htole32 (di->i_mtime); > > + else > > + di->i_dtime = htole32 (0); > > + } > > + > > + /* We don't clear i_size, i_blocks, and i_translator if mode is 0, > > + to give "undeletion" utilities a chance. */ > > + if (st->st_mode != 0) > > { > > - 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 */ > > diff --git a/ext2fs/orphan.c b/ext2fs/orphan.c > > new file mode 100644 > > index 000000000..bdd0eb90b > > --- /dev/null > > +++ b/ext2fs/orphan.c > > @@ -0,0 +1,215 @@ > > +/* 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 mount, the list is > > + traversed and each orphan is cleaned up (truncated and freed). > > + > > + 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 <pthread.h> > > + > > +/* Dedicated mutex to protect the Ext3 orphan linked list and > s_last_orphan.*/ > > +static pthread_mutex_t orphan_lock = PTHREAD_MUTEX_INITIALIZER; > > + > > +/* 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; > > + > > + assert_backtrace (!diskfs_readonly); > > + assert_backtrace (np->dn_stat.st_nlink == 0); > > + > > + if (diskfs_node_disknode (np)->on_orphan_list) > > + return; > > + > > + ext2_debug ("adding inode %lu to orphan list", (unsigned long) inum); > > + > > + txn = diskfs_journal_start_transaction (); > > + > > + pthread_mutex_lock (&orphan_lock); > > + > > + di = dino_ref (inum); > > + > > + /* Set i_dtime to the current s_last_orphan (the old list head). */ > > + di->i_dtime = sblock->s_last_orphan; > > + di->i_links_count = 0; > > + > > + /* 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); > > +} > > + > > +/* Remove inode NP from the orphan list. */ > > +void > > +diskfs_orphan_del (struct node *np) > > +{ > > + if (!ext2_journal) > > + return; > > + > > + ino_t inum = np->cache_id; > > + __u32 prev_orphan; > > + diskfs_transaction_t *txn = NULL; > > + > > + if (!diskfs_node_disknode (np)->on_orphan_list) > > + return; > > + > > + ext2_debug ("removing inode %lu from orphan list", (unsigned > long)inum); > > + > > + txn = diskfs_journal_start_transaction (); > > + > > + pthread_mutex_lock (&orphan_lock); > > + > > + struct ext2_inode *my_di = dino_ref (inum); > > + __u32 my_next = le32toh (my_di->i_dtime); > > + > > + /* Scrub our own i_dtime. If we don't, e2fsck's Pass 1 will see the > leftover > > + inode number (< s_inodes_count), mistake it for a stale linked-list > > + pointer, and declare the inode a refugee of a corrupted orphan > list! */ > > + my_di->i_dtime = 0; > > + > > + if (txn) > > + journal_dirty_block (txn, boffs_block (bptr_offs (my_di))); > > + > > + dino_deref (my_di); > > + > > + prev_orphan = le32toh (sblock->s_last_orphan); > > + > > + if (prev_orphan == inum) > > + { > > + sblock->s_last_orphan = htole32 (my_next); > > + sblock_dirty = 1; > > + > > + if (txn) > > + { > > + memcpy (boffs_ptr (SBLOCK_OFFS), sblock, SBLOCK_SIZE); > > + journal_dirty_block (txn, boffs_block (SBLOCK_OFFS)); > > + } > > + } > > + else > > + { > > + /* Walk the list to find the predecessor. */ > > + while (prev_orphan != 0) > > + { > > + struct ext2_inode *prev_di = dino_ref (prev_orphan); > > + __u32 next = le32toh (prev_di->i_dtime); > > + > > + if (next == inum) > > + { > > + /* Found the predecessor. Update its i_dtime to skip over > us. */ > > + prev_di->i_dtime = htole32 (my_next); > > + > > + if (txn) > > + journal_dirty_block (txn, boffs_block (bptr_offs > (prev_di))); > > + > > + dino_deref (prev_di); > > + break; > > + } > > + dino_deref (prev_di); > > + prev_orphan = next; > > + } > > + } > > + > > + diskfs_node_disknode (np)->on_orphan_list = 0; > > + > > + pthread_mutex_unlock (&orphan_lock); > > + > > + if (txn) > > + diskfs_journal_stop_transaction (txn); > > + else > > + alloc_sync (np); > > +} > > + > > +/* 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; > > + } > > + > > + ino_t inum; > > + __u32 next_orphan; > > + int count = 0; > > + struct ext2_inode *di; > > + struct node *np = NULL; > > + error_t err; > > + > > + next_orphan = le32toh (sblock->s_last_orphan); > > + > > + if (next_orphan == 0) > > + return; > > + > > + ext2_warning ("recovering orphan inode list (head=%u)", next_orphan); > > + > > + while (next_orphan != 0) > > + { > > + inum = next_orphan; > > + > > + /* Grab the next pointer directly from the disk structure */ > > + 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, strerror (err)); > > + continue; > > + } > > + diskfs_node_disknode (np)->on_orphan_list = 1; > > + > > + diskfs_nput (np); > > + count++; > > + } > > + > > + if (count > 0) > > + ext2_warning ("recovered %d orphan inode(s)", count); > > +} > > 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-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..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); > > 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); > > @@ -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/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..39eaf4bf7 100644 > > --- a/libdiskfs/dir-unlink.c > > +++ b/libdiskfs/dir-unlink.c > > @@ -82,7 +82,10 @@ diskfs_S_dir_unlink (struct protid *dircred, > > diskfs_node_update (np, diskfs_synchronous); > > > > if (np->dn_stat.st_nlink == 0) > > - fshelp_fetch_control (&np->transbox, &control); > > + { > > + diskfs_orphan_add (np); > > + fshelp_fetch_control (&np->transbox, &control); > > + } > > > > /* This check is necessary because we might get here on an error while > > checking the mode on something which happens to be `.'. */ > > 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-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 > > 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); > > 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 > PS> Salut ! J'ai un sujet de philo à vous soumettre : "Suffit-il > PS> d'observer pour connaître" Idées + plan Merçi > Oui, ya qu'a t'observer pour connaître le fait que tu es une feignasse. > -+- FF in: Guide du Neuneu d'Usenet - Neuneu fait de la philo -+- >
