From 012726f27236ef8bf8f15af0b801d189cb67bfd2 Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Wed, 24 Jun 2026 23:47:52 +1000 Subject: [PATCH 1/3] Align getdents64 records to fix unaligned access on MIPS getdents64 wrote linux_dirent64 records with d_reclen = header + name, without rounding up to the alignment of the leading u64 d_ino. The next record's d_ino then lands unaligned, and a strict-alignment guest (e.g. MIPS/MIPS64) faults with UC_ERR_READ_UNALIGNED when walking the buffer. x86 tolerates the unaligned load, which is why this was never caught. Round d_reclen up to the d_ino alignment, matching the kernel. The rounding is scoped to the getdents64 (is_64) branch only: getdents64 places d_type before d_name so the trailing pad bytes are inert, whereas the legacy getdents layout stores d_type at offset d_reclen-1 and would break if padded the same way -- that is what got the earlier blanket fix (PR #1419) reverted. Co-Authored-By: Claude Opus 4.8 (1M context) --- qiling/os/posix/syscall/unistd.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qiling/os/posix/syscall/unistd.py b/qiling/os/posix/syscall/unistd.py index 09b86c380..8076dcd7c 100644 --- a/qiling/os/posix/syscall/unistd.py +++ b/qiling/os/posix/syscall/unistd.py @@ -936,6 +936,18 @@ def _type_mapping(ent): packed_d_ino = (ql.pack64(d_ino), n) if is_64: + # The kernel rounds each linux_dirent64 record up to the + # alignment of its leading u64 d_ino, so the next record's d_ino + # stays aligned. Without this, strict-alignment guests (e.g. + # MIPS) fault with an unaligned load when walking the buffer; + # x86 tolerates it, which is why it was never caught. This is + # safe here because getdents64 places d_type *before* d_name, so + # the trailing pad bytes are inert. The legacy getdents layout + # below stores d_type at offset d_reclen-1, so it can't be + # padded the same way without relocating d_type -- that mistake + # is what got the earlier blanket fix (PR #1419) reverted. + d_reclen = (d_reclen + (n - 1)) & ~(n - 1) + fields = ( (ql.pack64(d_ino), n), (ql.pack64(d_off), n), From 0e48da4be89d1d8a1708cef238c081840ecc917a Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Thu, 25 Jun 2026 00:31:16 +1000 Subject: [PATCH 2/3] Add regression test for getdents64 record alignment Calls ql_syscall_getdents64 on a directory and walks the returned linux_dirent64 records, asserting every record (and thus its leading u64 d_ino) starts on an 8-byte boundary. Before the fix d_reclen was not rounded up, so records were misaligned and a strict-alignment guest (e.g. MIPS) faulted with an unaligned load. Self-contained; runs on stock unicorn. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_elf.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_elf.py b/tests/test_elf.py index 2798028b7..052ba80b3 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -725,6 +725,43 @@ def test_elf_linux_x86_getdents64(self): del ql + # Regression for getdents64 record alignment. Each linux_dirent64 record + # must be padded so the next record's leading u64 d_ino stays 8-byte + # aligned; otherwise a strict-alignment guest (e.g. MIPS) faults with an + # unaligned load while walking the buffer. x86 tolerates the unaligned read, + # which is why it was never caught. + def test_linux_getdents64_alignment(self): + from qiling.const import QL_ENDIAN + from qiling.os.posix.syscall.fcntl import ql_syscall_open + from qiling.os.posix.syscall.unistd import ql_syscall_getdents64 + + ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX, + endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips32_linux", + verbose=QL_VERBOSE.OFF) + + base = 0x100000 + ql.mem.map(base, 0x4000) + path_ptr, buf_ptr = base, base + 0x100 + ql.mem.write(path_ptr, b"/\x00") + + fd = ql_syscall_open(ql, path_ptr, 0, 0) # O_RDONLY on a directory + self.assertGreaterEqual(fd, 0) + + nbytes = ql_syscall_getdents64(ql, fd, buf_ptr, 0x2000) + self.assertGreater(nbytes, 0) + + # walk the linux_dirent64 records; every record (hence its leading u64 + # d_ino) must start at an 8-byte boundary. d_reclen is a u16 at offset 16. + buf = bytes(ql.mem.read(buf_ptr, nbytes)) + off = 0 + while off < nbytes: + self.assertEqual(off % 8, 0, f"dirent at offset {off} is not 8-byte aligned") + d_reclen = int.from_bytes(buf[off + 16:off + 18], "big") + self.assertNotEqual(d_reclen, 0) + off += d_reclen + + del ql + def test_memory_search(self): ql = Qiling(code=b"\xCC", archtype=QL_ARCH.X8664, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.DEBUG) From a4ca725e4ee9553df2de9b4b8aad7fa09d787392 Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Thu, 25 Jun 2026 21:55:21 +1000 Subject: [PATCH 3/3] Align legacy getdents records too Extend the record alignment to the legacy getdents path. The kernel rounds both getdents and getdents64 records up to the alignment of their leading d_ino (ALIGN(reclen, sizeof(long))); without it a strict-alignment guest (MIPS) faults walking the buffer, e.g. older glibc busybox 'ls' (which uses the legacy getdents syscall) crashes on directory listings. Legacy linux_dirent stores d_type in the record's last byte (offset d_reclen-1), so the alignment padding goes between d_name and d_type to keep d_type there -- padding without relocating d_type is what got the earlier blanket attempt (PR #1419) reverted. getdents64 (d_type before d_name) is unchanged. Add test_linux_getdents_alignment: walks legacy getdents records on a big-endian MIPS guest and asserts each is aligned, tiles the buffer, and keeps d_type (DT_DIR for '.'/'..'). Runs on stock unicorn. Co-Authored-By: Claude Opus 4.8 (1M context) --- qiling/os/posix/syscall/unistd.py | 31 +++++++++++--------- tests/test_elf.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/qiling/os/posix/syscall/unistd.py b/qiling/os/posix/syscall/unistd.py index 8076dcd7c..62cb6f61a 100644 --- a/qiling/os/posix/syscall/unistd.py +++ b/qiling/os/posix/syscall/unistd.py @@ -926,7 +926,15 @@ def _type_mapping(ent): d_off = 0 d_name = (result.name if isinstance(result, os.DirEntry) else result._str).encode() + b'\x00' d_type = _type_mapping(result) - d_reclen = n + n + 2 + len(d_name) + 1 + + # The kernel rounds each record up to the alignment of its leading + # (pointer-sized) d_ino, so the next record's d_ino stays aligned. + # Without this, strict-alignment guests (e.g. MIPS) fault with an + # unaligned load when walking the buffer; x86 tolerates it, which is + # why it was never caught. Matches the kernel's ALIGN(reclen, + # sizeof(long)) for both getdents and getdents64. + unaligned_reclen = n + n + 2 + len(d_name) + 1 + d_reclen = (unaligned_reclen + (n - 1)) & ~(n - 1) # TODO: Dirty fix for X8664 MACOS 11.6 APFS # For some reason MACOS return int value is 64bit @@ -936,18 +944,8 @@ def _type_mapping(ent): packed_d_ino = (ql.pack64(d_ino), n) if is_64: - # The kernel rounds each linux_dirent64 record up to the - # alignment of its leading u64 d_ino, so the next record's d_ino - # stays aligned. Without this, strict-alignment guests (e.g. - # MIPS) fault with an unaligned load when walking the buffer; - # x86 tolerates it, which is why it was never caught. This is - # safe here because getdents64 places d_type *before* d_name, so - # the trailing pad bytes are inert. The legacy getdents layout - # below stores d_type at offset d_reclen-1, so it can't be - # padded the same way without relocating d_type -- that mistake - # is what got the earlier blanket fix (PR #1419) reverted. - d_reclen = (d_reclen + (n - 1)) & ~(n - 1) - + # getdents64 places d_type *before* d_name, so the trailing pad + # bytes are inert. fields = ( (ql.pack64(d_ino), n), (ql.pack64(d_off), n), @@ -956,11 +954,18 @@ def _type_mapping(ent): (d_name, len(d_name)) ) else: + # legacy linux_dirent stores d_type in the record's last byte + # (offset d_reclen-1), so the alignment padding goes *between* + # d_name and d_type to keep d_type there. padding without + # relocating d_type is what got the earlier blanket fix + # (PR #1419) reverted. + pad = d_reclen - unaligned_reclen fields = ( packed_d_ino, (ql.pack(d_off), n), (ql.pack16(d_reclen), 2), (d_name, len(d_name)), + (b'\x00' * pad, pad), (d_type, 1) ) diff --git a/tests/test_elf.py b/tests/test_elf.py index 052ba80b3..42f702672 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -762,6 +762,53 @@ def test_linux_getdents64_alignment(self): del ql + def test_linux_getdents_alignment(self): + # the legacy getdents record was not rounded up to the alignment of its + # word-sized d_ino either, so a strict-alignment guest faulted walking + # the buffer. unlike getdents64, legacy linux_dirent keeps d_type in the + # record's last byte (offset d_reclen-1), so the padding must sit between + # d_name and d_type -- verify the records are aligned AND d_type survives. + from qiling.const import QL_ENDIAN + from qiling.os.posix.syscall.fcntl import ql_syscall_open + from qiling.os.posix.syscall.unistd import ql_syscall_getdents + + ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX, + endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips32_linux", + verbose=QL_VERBOSE.OFF) + + base = 0x100000 + ql.mem.map(base, 0x4000) + path_ptr, buf_ptr = base, base + 0x100 + ql.mem.write(path_ptr, b"/\x00") + + fd = ql_syscall_open(ql, path_ptr, 0, 0) # O_RDONLY on a directory + self.assertGreaterEqual(fd, 0) + + nbytes = ql_syscall_getdents(ql, fd, buf_ptr, 0x2000) + self.assertGreater(nbytes, 0) + + # walk the linux_dirent records; on 32-bit MIPS d_ino is 4 bytes, so each + # record must start at a 4-byte boundary. d_reclen is a u16 at offset 8 + # (after the 4-byte d_ino + 4-byte d_off); d_name starts at offset 10. + buf = bytes(ql.mem.read(buf_ptr, nbytes)) + off = 0 + while off < nbytes: + self.assertEqual(off % 4, 0, f"dirent at offset {off} is not 4-byte aligned") + d_reclen = int.from_bytes(buf[off + 8:off + 10], "big") + self.assertNotEqual(d_reclen, 0) + + name = buf[off + 10:buf.index(b"\x00", off + 10)].decode() + # legacy getdents stores d_type in the record's last byte; '.' and + # '..' must still report DT_DIR (4) after the alignment padding + if name in (".", ".."): + self.assertEqual(buf[off + d_reclen - 1], 4, f"{name}: d_type not DT_DIR") + + off += d_reclen + + self.assertEqual(off, nbytes) # records tile the buffer exactly + + del ql + def test_memory_search(self): ql = Qiling(code=b"\xCC", archtype=QL_ARCH.X8664, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.DEBUG)