33
44#include "lcfs-writer.h"
55#include "lcfs-mount.h"
6+ #include "lcfs-erofs.h"
7+ #include "erofs_fs_wrapper.h"
68#include <string.h>
79#include <assert.h>
810#include <unistd.h>
911#include <errno.h>
12+ #include <endian.h>
13+ #include <sys/stat.h>
1014
1115static inline void lcfs_node_unrefp (struct lcfs_node_s * * nodep )
1216{
@@ -121,6 +125,106 @@ static void test_add_uninitialized_child(void)
121125 assert (errno == EINVAL );
122126}
123127
128+ /* Regression test for heap-use-after-free when loading an EROFS image that
129+ * contains a hardlinked whiteout (chardev with rdev=0, nlink>1).
130+ *
131+ * A whiteout represents the absence of a file, so nlink>1 is semantically
132+ * invalid. The loader must reject such images with EINVAL rather than
133+ * silently processing them (which previously caused a use-after-free via a
134+ * stale node_hash entry when the alias dirent appeared before the canonical
135+ * one in the directory block).
136+ *
137+ * We construct a minimal EROFS image in memory rather than loading a binary
138+ * fixture, so the test is self-contained.
139+ */
140+ static void test_hardlinked_whiteout_load (void )
141+ {
142+ /*
143+ * Image layout (2 blocks = 8192 bytes, all in block 0's metadata area):
144+ *
145+ * 0x000 lcfs_erofs_header_s (composefs header, 32 bytes)
146+ * 0x400 erofs_super_block (EROFS superblock, 128 bytes)
147+ * 0x480 erofs_inode_compact root dir, nid=36, 32 bytes
148+ * 0x4A0 inline dir data 3 dirents + names, 41 bytes
149+ * 0x4E0 erofs_inode_compact whiteout, nid=39, 32 bytes
150+ */
151+ uint8_t image [2 * EROFS_BLKSIZ ];
152+ memset (image , 0 , sizeof (image ));
153+
154+ /* Composefs header at offset 0 */
155+ struct lcfs_erofs_header_s * cfs = (struct lcfs_erofs_header_s * )image ;
156+ cfs -> magic = htole32 (LCFS_EROFS_MAGIC );
157+ cfs -> version = htole32 (LCFS_EROFS_VERSION );
158+
159+ /* EROFS superblock at offset 1024 */
160+ struct erofs_super_block * sb =
161+ (struct erofs_super_block * )(image + EROFS_SUPER_OFFSET );
162+ sb -> magic = htole32 (EROFS_SUPER_MAGIC_V1 );
163+ sb -> blkszbits = EROFS_BLKSIZ_BITS ;
164+ sb -> root_nid = htole16 (36 ); /* nid=36 → offset 36*32 = 0x480 */
165+ sb -> inos = htole64 (2 );
166+ sb -> blocks = htole32 (2 );
167+ sb -> meta_blkaddr = htole32 (0 );
168+ sb -> xattr_blkaddr = htole32 (0 );
169+
170+ /* Root directory inode (compact, 32 bytes) at offset 0x480, nid=36.
171+ * Data layout = FLAT_INLINE (tailpacked dir entries follow the inode). */
172+ const uint16_t root_nid = 36 ;
173+ const uint16_t wh_nid = 39 ; /* offset 39*32 = 0x4E0 */
174+ struct erofs_inode_compact * root_ino =
175+ (struct erofs_inode_compact * )(image + root_nid * EROFS_SLOTSIZE );
176+ root_ino -> i_format = htole16 ((EROFS_INODE_FLAT_INLINE << EROFS_I_DATALAYOUT_BIT ) |
177+ (EROFS_INODE_LAYOUT_COMPACT << EROFS_I_VERSION_BIT ));
178+ root_ino -> i_mode = htole16 (S_IFDIR | 0755 );
179+ root_ino -> i_nlink = htole16 (2 );
180+
181+ /* Build inline directory data right after the root inode (offset 0x4A0).
182+ * 3 entries: "." (self), ".." (parent), "wh" (whiteout child).
183+ * Each dirent is 12 bytes; names start at offset 3*12 = 36. */
184+ uint8_t * dir = image + root_nid * EROFS_SLOTSIZE +
185+ sizeof (struct erofs_inode_compact );
186+ const uint16_t names_off = 3 * sizeof (struct erofs_dirent ); /* 36 */
187+ /* "." at offset 36, ".." at 37, "wh" at 39 → total = 41 bytes */
188+ const uint32_t dir_size = names_off + 1 + 2 + 2 ; /* 41 */
189+ root_ino -> i_size = htole32 (dir_size );
190+
191+ struct erofs_dirent * de = (struct erofs_dirent * )dir ;
192+ /* dirent[0]: "." */
193+ de [0 ].nid = htole64 (root_nid );
194+ de [0 ].nameoff = htole16 (names_off );
195+ de [0 ].file_type = EROFS_FT_DIR ;
196+ /* dirent[1]: ".." */
197+ de [1 ].nid = htole64 (root_nid );
198+ de [1 ].nameoff = htole16 (names_off + 1 );
199+ de [1 ].file_type = EROFS_FT_DIR ;
200+ /* dirent[2]: "wh" */
201+ de [2 ].nid = htole64 (wh_nid );
202+ de [2 ].nameoff = htole16 (names_off + 3 );
203+ de [2 ].file_type = EROFS_FT_CHRDEV ;
204+
205+ memcpy (dir + names_off , "." , 1 );
206+ memcpy (dir + names_off + 1 , ".." , 2 );
207+ memcpy (dir + names_off + 3 , "wh" , 2 );
208+
209+ /* Whiteout inode (compact, 32 bytes) at offset 0x4E0, nid=39.
210+ * chardev with rdev=0 and nlink=252 (>1 triggers EINVAL). */
211+ struct erofs_inode_compact * wh_ino =
212+ (struct erofs_inode_compact * )(image + wh_nid * EROFS_SLOTSIZE );
213+ wh_ino -> i_format = htole16 ((EROFS_INODE_FLAT_PLAIN << EROFS_I_DATALAYOUT_BIT ) |
214+ (EROFS_INODE_LAYOUT_COMPACT << EROFS_I_VERSION_BIT ));
215+ wh_ino -> i_mode = htole16 (S_IFCHR );
216+ wh_ino -> i_nlink = htole16 (252 );
217+ wh_ino -> i_u .rdev = htole32 (0 ); /* rdev=0 makes it a whiteout */
218+
219+ /* The loader must reject this image with EINVAL (hardlinked whiteout)
220+ * and must not crash (the original bug was a use-after-free). */
221+ cleanup_node struct lcfs_node_s * root =
222+ lcfs_load_node_from_image (image , sizeof (image ));
223+ int errsv = errno ;
224+ assert (root == NULL );
225+ assert (errsv == EINVAL );
226+ }
227+
124228// Verifies that lcfs_fd_measure_fsverity fails on a fd without fsverity
125229static void test_no_verity (void )
126230{
@@ -140,9 +244,13 @@ static void test_no_verity(void)
140244
141245int main (int argc , char * * argv )
142246{
247+ (void )argc ;
248+ (void )argv ;
249+
143250 test_basic ();
144251 test_no_verity ();
145252 test_add_uninitialized_child ();
146253 test_xattr_addremove ();
147254 test_xattr_doubleadd ();
255+ test_hardlinked_whiteout_load ();
148256}
0 commit comments