Merge pull request #1636 from retrocpugeek/fix/getdents64-alignment

Align getdents and getdents64 records to fix unaligned access on MIPS
This commit is contained in:
xwings
2026-09-05 20:51:08 +08:00
committed by GitHub
2 changed files with 102 additions and 1 deletions
+18 -1
View File
@@ -926,7 +926,15 @@ def __getdents_common(ql: Qiling, fd: int, dirp: int, count: int, *, is_64: bool
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 __getdents_common(ql: Qiling, fd: int, dirp: int, count: int, *, is_64: bool
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 __getdents_common(ql: Qiling, fd: int, dirp: int, count: int, *, is_64: bool
(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)
)
+84
View File
@@ -725,6 +725,90 @@ class ELFTest(unittest.TestCase):
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)