Skip to content

getdents / getdents64 return unaligned dirent records (unaligned-load fault on MIPS) #1635

Description

@retrocpugeek

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_getdents64ql_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:

  1. (d_reclen + n) over-pads; the canonical round-up is (d_reclen + (n - 1)) & ~(n - 1).
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions