getdents / getdents64 return unaligned dirent records (unaligned-load fault on MIPS)
Describe the bug
__getdents_common (qiling/os/posix/syscall/unistd.py) computes each record length as
d_reclen = n + n + 2 + len(d_name) + 1
without rounding it up. The kernel rounds each record up to the alignment of its leading d_ino, so the next record's d_ino stays aligned. Without that, records pack tightly and the next d_ino lands on a misaligned address. A strict-alignment guest (e.g. MIPS / MIPS64) then faults with an unaligned load (UC_ERR_*_UNALIGNED) while walking the buffer — e.g. busybox ls crashes right after the syscall. x86 tolerates the unaligned read, which is why it's invisible there.
This affects both dirent syscalls:
getdents64 — leading u64 d_ino (needs 8-byte alignment on every arch).
- legacy
getdents — word-sized d_ino: 4 bytes on a 32-bit guest, 8 bytes on a 64-bit guest (e.g. MIPS64 n64). Older glibc still issues this syscall, so MIPS64 directory listings fault here too.
Repro (Python) — getdents64
from qiling import Qiling
from qiling.const import QL_ARCH, QL_OS, QL_ENDIAN, QL_VERBOSE
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)
ql.mem.write(base, b"/\x00")
fd = ql_syscall_open(ql, base, 0, 0)
n = ql_syscall_getdents64(ql, fd, base + 0x100, 0x2000)
buf = bytes(ql.mem.read(base + 0x100, n))
off = 0
while off < n:
print("record at offset", off, "aligned" if off % 8 == 0 else "*** MISALIGNED ***")
off += int.from_bytes(buf[off + 16:off + 18], "big") # d_reclen (u16 @ 16)
Output (without the fix):
record at offset 0 aligned
record at offset 21 *** MISALIGNED *** # ".." starts at 21 -> its u64 d_ino is unaligned
record at offset 43 *** MISALIGNED ***
...
The legacy getdents path has the identical defect (swap ql_syscall_getdents64 → ql_syscall_getdents; d_reclen is a u16 at offset 2 * pointersize). On MIPS the guest's load of the misaligned d_ino raises UC_ERR_*_UNALIGNED.
Expected behavior
Each record should be padded so the next record's leading d_ino is aligned, matching the kernel's ALIGN(reclen, sizeof(long)). All record offsets land on aligned boundaries and a strict-alignment guest can walk the buffer.
Why it was never caught
x86 tolerates unaligned loads. The existing getdents test binaries (test_elf_linux_x8664_getdents, test_elf_linux_x86_getdents64) drive the legacy getdents path but only assert directory names, never the record alignment / d_type, so the defect is invisible in CI.
Prior art (please read before reviewing)
This bug was fixed once and reverted:
That blanket approach has two problems the fix here avoids:
(d_reclen + n) over-pads; the canonical round-up is (d_reclen + (n - 1)) & ~(n - 1).
- It rounded the legacy
getdents record unconditionally without relocating d_type. The legacy linux_dirent stores d_type in the record's last byte (offset d_reclen - 1); padding the record moved the consumer's d_type read into the pad bytes — almost certainly the cause of the revert. (getdents64 is safe because it puts d_type before d_name, so trailing pad is inert.)
A fix for both layouts is proposed in PR #1636 (supersedes #1425).
getdents/getdents64return unaligned dirent records (unaligned-load fault on MIPS)Describe the bug
__getdents_common(qiling/os/posix/syscall/unistd.py) computes each record length aswithout rounding it up. The kernel rounds each record up to the alignment of its leading
d_ino, so the next record'sd_inostays aligned. Without that, records pack tightly and the nextd_inolands on a misaligned address. A strict-alignment guest (e.g. MIPS / MIPS64) then faults with an unaligned load (UC_ERR_*_UNALIGNED) while walking the buffer — e.g.busybox lscrashes right after the syscall. x86 tolerates the unaligned read, which is why it's invisible there.This affects both dirent syscalls:
getdents64— leadingu64 d_ino(needs 8-byte alignment on every arch).getdents— word-sizedd_ino: 4 bytes on a 32-bit guest, 8 bytes on a 64-bit guest (e.g. MIPS64 n64). Older glibc still issues this syscall, so MIPS64 directory listings fault here too.Repro (Python) —
getdents64Output (without the fix):
The legacy
getdentspath has the identical defect (swapql_syscall_getdents64→ql_syscall_getdents;d_reclenis a u16 at offset2 * pointersize). On MIPS the guest's load of the misalignedd_inoraisesUC_ERR_*_UNALIGNED.Expected behavior
Each record should be padded so the next record's leading
d_inois aligned, matching the kernel'sALIGN(reclen, sizeof(long)). All record offsets land on aligned boundaries and a strict-alignment guest can walk the buffer.Why it was never caught
x86 tolerates unaligned loads. The existing getdents test binaries (
test_elf_linux_x8664_getdents,test_elf_linux_x86_getdents64) drive the legacygetdentspath but only assert directory names, never the record alignment /d_type, so the defect is invisible in CI.Prior art (please read before reviewing)
This bug was fixed once and reverted:
d_reclen = (d_reclen + n) & ~(n - 1), then Revert "Bugfix: unaligned access in MIPS getdents64" #1423 reverted it ("seems like there is an issue"). Bugfix: unaligned access in MIPS getdents64 #1425 re-submits the same formula and is still open.That blanket approach has two problems the fix here avoids:
(d_reclen + n)over-pads; the canonical round-up is(d_reclen + (n - 1)) & ~(n - 1).getdentsrecord unconditionally without relocatingd_type. The legacylinux_direntstoresd_typein the record's last byte (offsetd_reclen - 1); padding the record moved the consumer'sd_typeread into the pad bytes — almost certainly the cause of the revert. (getdents64is safe because it putsd_typebefored_name, so trailing pad is inert.)A fix for both layouts is proposed in PR #1636 (supersedes #1425).