Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion qiling/os/posix/syscall/unistd.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,15 @@ def _type_mapping(ent):
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
Expand All @@ -936,6 +944,8 @@ def _type_mapping(ent):
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),
Expand All @@ -944,11 +954,18 @@ def _type_mapping(ent):
(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)
)

Expand Down
84 changes: 84 additions & 0 deletions tests/test_elf.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,90 @@ def test_elf_linux_x86_getdents64(self):

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)

Expand Down