diff --git a/qiling/os/posix/syscall/stat.py b/qiling/os/posix/syscall/stat.py index 5b153100a..87530d50b 100644 --- a/qiling/os/posix/syscall/stat.py +++ b/qiling/os/posix/syscall/stat.py @@ -1413,17 +1413,45 @@ class Statx64(ctypes.Structure): _pack_ = 4 +# Big-endian counterparts of the statx structs. The kernel statx layout is the +# same on every architecture, so the big-endian variants reuse the little-endian +# field lists verbatim and only change the ctypes base class (and the nested +# timestamp type, which must itself be big-endian). Without these, statx() byte- +# swaps every field on a big-endian guest (e.g. MIPS/MIPS64 EB), so stx_mode +# loses its S_IFDIR bit and tools like `ls` treat directories as plain files. +class StatxTimestamp32EB(ctypes.BigEndianStructure): + _fields_ = StatxTimestamp32._fields_ + +class StatxTimestamp64EB(ctypes.BigEndianStructure): + _fields_ = StatxTimestamp64._fields_ + +def _statx_fields_eb(fields): + swap = {StatxTimestamp32: StatxTimestamp32EB, StatxTimestamp64: StatxTimestamp64EB} + return [(name, swap.get(ftype, ftype)) for (name, ftype) in fields] + +class Statx32EB(ctypes.BigEndianStructure): + _fields_ = _statx_fields_eb(Statx32._fields_) + _pack_ = 8 + +class Statx64EB(ctypes.BigEndianStructure): + _fields_ = _statx_fields_eb(Statx64._fields_) + _pack_ = 4 + # int statx(int dirfd, const char *restrict pathname, int flags, # unsigned int mask, struct statx *restrict statxbuf); def ql_syscall_statx(ql: Qiling, dirfd: int, path: int, flags: int, mask: int, buf_ptr: int): + is_eb = ql.arch.endian == QL_ENDIAN.EB + def statx_convert_timestamp(tv_sec, tv_nsec): tv_sec = struct.unpack('i', struct.pack('f', tv_sec))[0] tv_nsec = struct.unpack('i', struct.pack('f', tv_nsec))[0] if ql.arch.bits == 32: - return StatxTimestamp32(tv_sec=tv_sec, tv_nsec=tv_nsec) + Timestamp = StatxTimestamp32EB if is_eb else StatxTimestamp32 else: - return StatxTimestamp64(tv_sec=tv_sec, tv_nsec=tv_nsec) + Timestamp = StatxTimestamp64EB if is_eb else StatxTimestamp64 + + return Timestamp(tv_sec=tv_sec, tv_nsec=tv_nsec) def major(dev): @@ -1438,9 +1466,9 @@ def minor(dev): st = Stat(real_path, fd) if ql.arch.bits == 32: - Statx = Statx32 + Statx = Statx32EB if is_eb else Statx32 else: - Statx = Statx64 + Statx = Statx64EB if is_eb else Statx64 stx = Statx( stx_mask = 0x07ff, # STATX_BASIC_STATS diff --git a/tests/test_elf.py b/tests/test_elf.py index 2798028b7..9b0902226 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -725,6 +725,37 @@ def test_elf_linux_x86_getdents64(self): del ql + # Regression for statx() byte order on big-endian guests: the statx struct + # must be serialized in the guest's endianness. Otherwise stx_mode is byte- + # swapped and a directory loses its S_IFDIR bit (so e.g. `ls` treats a + # directory as a regular file). Exercised on a big-endian MIPS context. + def test_linux_statx_bigendian(self): + import stat as _stat + from qiling.const import QL_ENDIAN + from qiling.os.posix.syscall.stat import ql_syscall_statx, Statx32 + + 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, 0x1000) + path_ptr, buf_ptr = base, base + 0x200 + ql.mem.write(path_ptr, b"/\x00") + + AT_FDCWD = -100 & 0xffffffff + STATX_BASIC_STATS = 0x07ff + ret = ql_syscall_statx(ql, AT_FDCWD, path_ptr, 0, STATX_BASIC_STATS, buf_ptr) + self.assertEqual(ret, 0) + + # the guest is big-endian, so it reads stx_mode in big-endian byte order; + # it must come back as a directory (S_IFDIR). Before the fix the struct + # was emitted little-endian and the type bits were lost. + mode = int.from_bytes(ql.mem.read(buf_ptr + Statx32.stx_mode.offset, 2), 'big') + self.assertTrue(_stat.S_ISDIR(mode)) + + del ql + def test_memory_search(self): ql = Qiling(code=b"\xCC", archtype=QL_ARCH.X8664, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.DEBUG)