diff --git a/qiling/os/posix/syscall/unistd.py b/qiling/os/posix/syscall/unistd.py index 09b86c380..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,6 +944,8 @@ def _type_mapping(ent): packed_d_ino = (ql.pack64(d_ino), n) if is_64: + # 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), @@ -944,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 2798028b7..42f702672 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -725,6 +725,90 @@ 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_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)