Skip to content

Commit a4ca725

Browse files
retrocpugeekclaude
andcommitted
Align legacy getdents records too
Extend the record alignment to the legacy getdents path. The kernel rounds both getdents and getdents64 records up to the alignment of their leading d_ino (ALIGN(reclen, sizeof(long))); without it a strict-alignment guest (MIPS) faults walking the buffer, e.g. older glibc busybox 'ls' (which uses the legacy getdents syscall) crashes on directory listings. 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 attempt (PR #1419) reverted. getdents64 (d_type before d_name) is unchanged. Add test_linux_getdents_alignment: walks legacy getdents records on a big-endian MIPS guest and asserts each is aligned, tiles the buffer, and keeps d_type (DT_DIR for '.'/'..'). Runs on stock unicorn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0e48da4 commit a4ca725

2 files changed

Lines changed: 65 additions & 13 deletions

File tree

qiling/os/posix/syscall/unistd.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,15 @@ def _type_mapping(ent):
926926
d_off = 0
927927
d_name = (result.name if isinstance(result, os.DirEntry) else result._str).encode() + b'\x00'
928928
d_type = _type_mapping(result)
929-
d_reclen = n + n + 2 + len(d_name) + 1
929+
930+
# The kernel rounds each record up to the alignment of its leading
931+
# (pointer-sized) d_ino, so the next record's d_ino stays aligned.
932+
# Without this, strict-alignment guests (e.g. MIPS) fault with an
933+
# unaligned load when walking the buffer; x86 tolerates it, which is
934+
# why it was never caught. Matches the kernel's ALIGN(reclen,
935+
# sizeof(long)) for both getdents and getdents64.
936+
unaligned_reclen = n + n + 2 + len(d_name) + 1
937+
d_reclen = (unaligned_reclen + (n - 1)) & ~(n - 1)
930938

931939
# TODO: Dirty fix for X8664 MACOS 11.6 APFS
932940
# For some reason MACOS return int value is 64bit
@@ -936,18 +944,8 @@ def _type_mapping(ent):
936944
packed_d_ino = (ql.pack64(d_ino), n)
937945

938946
if is_64:
939-
# The kernel rounds each linux_dirent64 record up to the
940-
# alignment of its leading u64 d_ino, so the next record's d_ino
941-
# stays aligned. Without this, strict-alignment guests (e.g.
942-
# MIPS) fault with an unaligned load when walking the buffer;
943-
# x86 tolerates it, which is why it was never caught. This is
944-
# safe here because getdents64 places d_type *before* d_name, so
945-
# the trailing pad bytes are inert. The legacy getdents layout
946-
# below stores d_type at offset d_reclen-1, so it can't be
947-
# padded the same way without relocating d_type -- that mistake
948-
# is what got the earlier blanket fix (PR #1419) reverted.
949-
d_reclen = (d_reclen + (n - 1)) & ~(n - 1)
950-
947+
# getdents64 places d_type *before* d_name, so the trailing pad
948+
# bytes are inert.
951949
fields = (
952950
(ql.pack64(d_ino), n),
953951
(ql.pack64(d_off), n),
@@ -956,11 +954,18 @@ def _type_mapping(ent):
956954
(d_name, len(d_name))
957955
)
958956
else:
957+
# legacy linux_dirent stores d_type in the record's last byte
958+
# (offset d_reclen-1), so the alignment padding goes *between*
959+
# d_name and d_type to keep d_type there. padding without
960+
# relocating d_type is what got the earlier blanket fix
961+
# (PR #1419) reverted.
962+
pad = d_reclen - unaligned_reclen
959963
fields = (
960964
packed_d_ino,
961965
(ql.pack(d_off), n),
962966
(ql.pack16(d_reclen), 2),
963967
(d_name, len(d_name)),
968+
(b'\x00' * pad, pad),
964969
(d_type, 1)
965970
)
966971

tests/test_elf.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,53 @@ def test_linux_getdents64_alignment(self):
762762

763763
del ql
764764

765+
def test_linux_getdents_alignment(self):
766+
# the legacy getdents record was not rounded up to the alignment of its
767+
# word-sized d_ino either, so a strict-alignment guest faulted walking
768+
# the buffer. unlike getdents64, legacy linux_dirent keeps d_type in the
769+
# record's last byte (offset d_reclen-1), so the padding must sit between
770+
# d_name and d_type -- verify the records are aligned AND d_type survives.
771+
from qiling.const import QL_ENDIAN
772+
from qiling.os.posix.syscall.fcntl import ql_syscall_open
773+
from qiling.os.posix.syscall.unistd import ql_syscall_getdents
774+
775+
ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX,
776+
endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips32_linux",
777+
verbose=QL_VERBOSE.OFF)
778+
779+
base = 0x100000
780+
ql.mem.map(base, 0x4000)
781+
path_ptr, buf_ptr = base, base + 0x100
782+
ql.mem.write(path_ptr, b"/\x00")
783+
784+
fd = ql_syscall_open(ql, path_ptr, 0, 0) # O_RDONLY on a directory
785+
self.assertGreaterEqual(fd, 0)
786+
787+
nbytes = ql_syscall_getdents(ql, fd, buf_ptr, 0x2000)
788+
self.assertGreater(nbytes, 0)
789+
790+
# walk the linux_dirent records; on 32-bit MIPS d_ino is 4 bytes, so each
791+
# record must start at a 4-byte boundary. d_reclen is a u16 at offset 8
792+
# (after the 4-byte d_ino + 4-byte d_off); d_name starts at offset 10.
793+
buf = bytes(ql.mem.read(buf_ptr, nbytes))
794+
off = 0
795+
while off < nbytes:
796+
self.assertEqual(off % 4, 0, f"dirent at offset {off} is not 4-byte aligned")
797+
d_reclen = int.from_bytes(buf[off + 8:off + 10], "big")
798+
self.assertNotEqual(d_reclen, 0)
799+
800+
name = buf[off + 10:buf.index(b"\x00", off + 10)].decode()
801+
# legacy getdents stores d_type in the record's last byte; '.' and
802+
# '..' must still report DT_DIR (4) after the alignment padding
803+
if name in (".", ".."):
804+
self.assertEqual(buf[off + d_reclen - 1], 4, f"{name}: d_type not DT_DIR")
805+
806+
off += d_reclen
807+
808+
self.assertEqual(off, nbytes) # records tile the buffer exactly
809+
810+
del ql
811+
765812
def test_memory_search(self):
766813
ql = Qiling(code=b"\xCC", archtype=QL_ARCH.X8664, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.DEBUG)
767814

0 commit comments

Comments
 (0)