* [PATCH e2fsprogs 1/2] debugfs: reject path-traversal names in rdump
2026-08-07 10:03 [PATCH e2fsprogs 0/2] debugfs: fix rdump path-traversal on untrusted images Nexory
@ 2026-08-07 10:03 ` Nexory
2026-08-07 10:03 ` [PATCH e2fsprogs 2/2] tests: add d_rdump_safe for the debugfs rdump path-traversal guard Nexory
2026-10-05 9:01 ` [PATCH e2fsprogs 0/2] debugfs: fix rdump path-traversal on untrusted images Theodore Tso
2 siblings, 0 replies; 4+ messages in thread
From: Nexory @ 2026-08-07 10:03 UTC (permalink / raw)
To: linux-ext4; +Cc: Theodore Ts'o, Nexory
rdump_dirent() copies the raw on-disk directory entry name and passes it to
rdump_inode(), which sprintf()s it into the destination path and open()s /
symlink()s the result. An entry named "../foo" (or one containing '/') in a
crafted or corrupted image therefore escapes the destination directory when
"rdump" extracts a tree from an untrusted filesystem image.
Reject such names in rdump_dirent() before use. Valid ext2/3/4 filesystems
never contain them, so there is no change for legitimate images; e2fsck
already treats '/' in a name as corruption (PR_2_BAD_NAME).
Signed-off-by: Nexory <St4yl3r30@hotmail.de>
---
debugfs/dump.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/debugfs/dump.c b/debugfs/dump.c
index cc81a50..e959cd0 100644
--- a/debugfs/dump.c
+++ b/debugfs/dump.c
@@ -317,6 +317,16 @@ static int rdump_dirent(struct ext2_dir_entry *dirent,
strncpy(name, dirent->name, thislen);
name[thislen] = 0;
+ /* Path-traversal guard: reject entry names that would escape the
+ * destination directory. A valid ext2/3/4 filesystem never contains
+ * a name with '/' or an empty name; e2fsck treats such names as
+ * corruption (PR_2_BAD_NAME). */
+ if (name[0] == 0 || strchr(name, '/')) {
+ com_err("rdump", 0, "skipping entry with unsafe name (inode %u)",
+ dirent->inode);
+ return 0;
+ }
+
if (debugfs_read_inode(dirent->inode, &inode, name))
return 0;
--
2.53.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH e2fsprogs 2/2] tests: add d_rdump_safe for the debugfs rdump path-traversal guard
2026-08-07 10:03 [PATCH e2fsprogs 0/2] debugfs: fix rdump path-traversal on untrusted images Nexory
2026-08-07 10:03 ` [PATCH e2fsprogs 1/2] debugfs: reject path-traversal names in rdump Nexory
@ 2026-08-07 10:03 ` Nexory
2026-10-05 9:01 ` [PATCH e2fsprogs 0/2] debugfs: fix rdump path-traversal on untrusted images Theodore Tso
2 siblings, 0 replies; 4+ messages in thread
From: Nexory @ 2026-08-07 10:03 UTC (permalink / raw)
To: linux-ext4; +Cc: Theodore Ts'o, Nexory
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 <St4yl3r30@hotmail.de>
---
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
^ permalink raw reply related [flat|nested] 4+ messages in thread