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.
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 list gives file system an opportunity to remove them on the following
restart prior to fsck run.
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.
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