[PATCH e2fsprogs 2/2] tests: add d_rdump_safe for the debugfs rdump path-traversal guard

Nexory <[email protected]> Fri, 7 Aug 2026 12:03:04 +0200
Newsgroups org.kernel.vger.linux-ext4
Message-ID <OS6P279MB1180BE9B7C39128DD2B6D891CBD12@OS6P279MB1180.NORP279.PROD.OUTLOOK.COM>
Extracts a crafted image whose root directory contains a normal file and a
hostile "../escaped" hard link to it. The test confirms rdump skips the
unsafe name, does not create a file outside the destination, and still dumps
the legitimate file. Without the guard the test fails (the entry escapes the
destination directory).

The malicious name contains a '/', which a valid filesystem never has and
which mke2fs/debugfs will not create, so the image is built with a small
libext2fs helper; mkimage.sh in the test directory documents the recipe.

Signed-off-by: Nexory <[email protected]>
---
 tests/d_rdump_safe/expect     |   6 ++++
 tests/d_rdump_safe/image.gz   | Bin 0 -> 922 bytes
 tests/d_rdump_safe/mkimage.sh |  55 ++++++++++++++++++++++++++++++++++
 tests/d_rdump_safe/name       |   1 +
 tests/d_rdump_safe/script     |  22 ++++++++++++++
 5 files changed, 84 insertions(+)
 create mode 100644 tests/d_rdump_safe/expect
 create mode 100644 tests/d_rdump_safe/image.gz
 create mode 100755 tests/d_rdump_safe/mkimage.sh
 create mode 100644 tests/d_rdump_safe/name
 create mode 100644 tests/d_rdump_safe/script

diff --git a/tests/d_rdump_safe/expect b/tests/d_rdump_safe/expect
new file mode 100644
index 0000000..fd8ed61
--- /dev/null
+++ b/tests/d_rdump_safe/expect
@@ -0,0 +1,6 @@
+debugfs rdump path-traversal guard
+debugfs -R ''rdump / DEST/out'' image
+rdump: skipping entry with unsafe name (inode NN)
+Exit status is 0
+escaped outside destination: no
+legitimate file dumped: yes
diff --git a/tests/d_rdump_safe/image.gz b/tests/d_rdump_safe/image.gz
new file mode 100644
index 0000000000000000000000000000000000000000..dd0e617255477017429477b603a409d382ac1f0a
GIT binary patch
literal 922
zcmb2|=3sdFwj_&*`R$$k*}{nu>>s|rdo;<7<)i!7e;s~JW>zBN(@Tm)SjsaWE2xIQ
z@Dyj4jfr{l;DtuVx~A^##s6D&a~xed+qp)6dR7p3Q^wOB=hba?Rv(T%A^WrKx$2!(
z29wbJHD1mZqT=%>2~2xrH1pnq=+JtjKYL>D@F$w=`fDngAKt6I`}bL2w&|<3^F^*%
zo1Pyp9~<KrAG6c+{{GL;PX4#JwtV($Zfo<ry2|hW9)5nkeqZji`P+?)W9q}r?IyL=
z^!IfK=}x_wbJWV@-2Y1!>m7`H7QH!NUT?zm>h+29_Zb&N9=h_UeEX~`|8w-ig%`y4
z+s~fK{de`x-174~GM~#cSX9WL`uF@+-CuvHXUn%)3%*WS^J(j+{fjPd6ib@hS!#A|
zXT)>$zh_#dzS`@{w-;30|7Sk)<$t|b{~3@#19Mp1<13t=w|;M&w`^}*DgXZDg>%<E
zl|K5k`e*aQtkU&+{4!LtE90iTmfu`<>2<7Z&YC;1@@uT72bdREf7(Ae^e4O2qNgGa
wf0#e<pRiZ>kIJ~>SgCvdk(Zqg+u=oTOOUJ`We%$l_;aoNSlGgy3=A9$01}hoO8@`>

literal 0
HcmV?d00001

diff --git a/tests/d_rdump_safe/mkimage.sh b/tests/d_rdump_safe/mkimage.sh
new file mode 100755
index 0000000..817f052
--- /dev/null
+++ b/tests/d_rdump_safe/mkimage.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+#
+# This is the script that was used to create the image.gz in this directory.
+#
+# The image is a tiny ext2 filesystem whose root directory holds a normal file
+# "loot" and a second, hostile directory entry whose name is the literal string
+# "../escaped" (a hard link to loot's inode).  A valid filesystem never contains
+# a name with a '/' in it, and neither mke2fs nor debugfs will create one, so the
+# hostile entry is planted with a small libext2fs helper: ext2fs_link() writes
+# the raw name without validating it.
+#
+# "debugfs -R 'rdump / <dest>'" used to copy that entry to <dest>/../escaped,
+# i.e. outside <dest>.  See the d_rdump_safe test.
+#
+# Requires mke2fs, debugfs and the libext2fs headers (the e2fsprogs build tree,
+# or the e2fslibs-dev / libext2fs-dev package).  The UUID and timestamps are not
+# pinned, so a re-run yields a functionally identical but not byte-identical
+# image, the same way the f_* fsck test images do.
+
+set -e -u
+
+IMG=image
+
+# 1. A tiny ext2 filesystem: 1024-byte blocks, 512 blocks, 64 inodes.
+mke2fs -F -q -b 1024 -N 64 "$IMG" 512
+
+# 2. A normal file "loot" in the root directory.
+printf 'loot-content\n' > loot-content
+debugfs -w -R "write loot-content loot" "$IMG"
+rm -f loot-content
+
+# 3. Plant a hostile hard link named "../escaped" onto loot's inode.
+cat > plant.c <<'PLANT'
+#include <ext2fs/ext2fs.h>
+
+int main(int argc, char **argv)
+{
+	ext2_filsys fs;
+	ext2_ino_t ino;
+
+	if (ext2fs_open(argv[1], EXT2_FLAG_RW, 0, 0, unix_io_manager, &fs))
+		return 1;
+	if (ext2fs_namei(fs, EXT2_ROOT_INO, EXT2_ROOT_INO, "loot", &ino))
+		return 1;
+	if (ext2fs_link(fs, EXT2_ROOT_INO, "../escaped", ino, EXT2_FT_REG_FILE))
+		return 1;
+	return ext2fs_close(fs) ? 1 : 0;
+}
+PLANT
+cc -o plant plant.c -lext2fs -lcom_err
+./plant "$IMG"
+rm -f plant plant.c
+
+# 4. Compress; the result is committed as image.gz.
+gzip -9 -f "$IMG"
diff --git a/tests/d_rdump_safe/name b/tests/d_rdump_safe/name
new file mode 100644
index 0000000..00d7a7e
--- /dev/null
+++ b/tests/d_rdump_safe/name
@@ -0,0 +1 @@
+debugfs rdump path-traversal guard
diff --git a/tests/d_rdump_safe/script b/tests/d_rdump_safe/script
new file mode 100644
index 0000000..f06637d
--- /dev/null
+++ b/tests/d_rdump_safe/script
@@ -0,0 +1,22 @@
+if ! test -x $DEBUGFS_EXE; then
+	echo "$test_name: $test_description: skipped (no debugfs)"
+	return 0
+fi
+OUT=$test_name.log
+EXP=$test_dir/expect
+echo "debugfs rdump path-traversal guard" > $OUT.new
+gunzip < $test_dir/image.gz > $TMPFILE
+DEST=$test_name.d
+rm -rf $DEST; mkdir -p $DEST/out
+echo "debugfs -R ''rdump / DEST/out'' image" >> $OUT.new
+$DEBUGFS -R "rdump / `pwd`/$DEST/out" $TMPFILE >> $OUT.new 2>&1
+echo "Exit status is $?" >> $OUT.new
+if test -e $DEST/escaped ; then echo "escaped outside destination: yes (VULNERABLE)" >> $OUT.new ; else echo "escaped outside destination: no" >> $OUT.new ; fi
+if test -e $DEST/out/loot ; then echo "legitimate file dumped: yes" >> $OUT.new ; else echo "legitimate file dumped: no" >> $OUT.new ; fi
+sed -f $cmd_dir/filter.sed $OUT.new | sed -e "s;`pwd`/;;g" -e "s/inode [0-9]*/inode NN/" > $OUT
+rm -rf $DEST $TMPFILE $OUT.new
+cmp -s $OUT $EXP
+status=$?
+if [ "$status" = 0 ] ; then echo "$test_name: $test_description: ok"; touch $test_name.ok
+else echo "$test_name: $test_description: failed"; diff $DIFF_OPTS $EXP $OUT > $test_name.failed; fi
+unset OUT EXP DEST
-- 
2.53.0