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 -+-

Reply via email to