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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions