diff --git a/.gitignore b/.gitignore index 766e541d9..b0cd99ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,7 @@ poetry.toml # LSP config files pyrightconfig.json + +# AI +CLAUDE.md +.claude diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..ba924a09a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,427 @@ +# Qiling Framework Architecture + +Qiling is a cross-platform, multi-architecture binary emulation framework built on top of the [Unicorn](https://www.unicorn-engine.org/) CPU emulation engine. It adds OS-level abstractions (syscalls, file systems, loaders) on top of raw CPU emulation, enabling full binary execution without native hardware. + +## High-Level Overview + +``` +┌──────────────────────────────────────────────────┐ +│ User Script / qltool │ +├──────────────────────────────────────────────────┤ +│ Qiling Core (core.py) │ +│ hooks · state snapshots · patches │ +├────────────┬─────────────┬───────────────────────┤ +│ OS Layer │ Loader │ Memory Manager │ +│ (QlOs) │ (QlLoader) │ (QlMemoryManager) │ +│ syscalls │ ELF/PE/ │ map · read · write │ +│ APIs │ MachO/etc │ MMIO callbacks │ +├────────────┴─────────────┴───────────────────────┤ +│ Architecture Layer (QlArch) │ +│ registers · disassembly · calling conventions │ +├──────────────────────────────────────────────────┤ +│ Unicorn Engine (CPU) │ +│ instruction-level emulation │ +└──────────────────────────────────────────────────┘ +``` + +## Directory Structure + +``` +qiling/ +├── qiling/ # Core framework package +│ ├── core.py # Qiling class — main entry point and orchestrator +│ ├── core_hooks.py # Hook system (code, memory, interrupt, address hooks) +│ ├── core_hooks_types.py # Hook type definitions and dispatch +│ ├── core_struct.py # Endian-aware struct packing utilities +│ ├── const.py # Enumerations: QL_ARCH, QL_OS, QL_ENDIAN, etc. +│ ├── exception.py # Custom exception hierarchy +│ ├── utils.py # Component selection (select_arch, select_os, etc.) +│ ├── host.py # Host platform interface +│ ├── log.py # Logging configuration +│ │ +│ ├── arch/ # Architecture implementations +│ │ ├── arch.py # QlArch — abstract base class +│ │ ├── x86.py # x86 / x86-64 / 8086 +│ │ ├── arm.py # ARMv7 (32-bit) +│ │ ├── arm64.py # ARMv8 (64-bit) +│ │ ├── mips.py # MIPS32 +│ │ ├── riscv.py # RISC-V 32-bit +│ │ ├── riscv64.py # RISC-V 64-bit +│ │ ├── ppc.py # PowerPC 32-bit +│ │ ├── cortex_m.py # ARM Cortex-M (MCU) +│ │ ├── register.py # Register management +│ │ └── models.py # CPU model definitions +│ │ +│ ├── os/ # Operating system implementations +│ │ ├── os.py # QlOs — abstract base class +│ │ ├── memory.py # QlMemoryManager +│ │ ├── fcall.py # Function call interface (read params, set return) +│ │ ├── mapper.py # Syscall/API mapping +│ │ ├── path.py # Virtual filesystem path resolution +│ │ ├── filestruct.py # File descriptor abstraction +│ │ ├── thread.py # Threading primitives +│ │ ├── posix/ # POSIX shared layer (syscall handlers) +│ │ ├── linux/ # Linux-specific OS +│ │ ├── freebsd/ # FreeBSD-specific OS +│ │ ├── macos/ # macOS-specific OS +│ │ ├── qnx/ # QNX RTOS +│ │ ├── windows/ # Windows (Win32/Win64 API emulation) +│ │ ├── uefi/ # UEFI firmware services +│ │ ├── dos/ # DOS (8086 interrupts) +│ │ ├── mcu/ # Bare-metal microcontroller +│ │ └── blob/ # Raw binary blob execution +│ │ +│ ├── loader/ # Binary format loaders +│ │ ├── loader.py # QlLoader — abstract base class +│ │ ├── elf.py # ELF (Linux, FreeBSD, QNX) +│ │ ├── pe.py # PE (Windows) +│ │ ├── pe_uefi.py # PE for UEFI +│ │ ├── macho.py # Mach-O (macOS) +│ │ ├── dos.py # DOS COM/EXE +│ │ ├── mcu.py # MCU firmware images +│ │ └── blob.py # Raw binary blobs +│ │ +│ ├── cc/ # Calling conventions +│ │ ├── intel.py # cdecl, stdcall, ms64 +│ │ ├── arm.py # aarch32, aarch64 +│ │ ├── mips.py # MIPS o32 +│ │ ├── riscv.py # RISC-V ABI +│ │ └── ppc.py # PowerPC ABI +│ │ +│ ├── hw/ # Hardware peripheral emulation (MCU) +│ │ ├── peripheral.py # Base peripheral class +│ │ ├── hw.py # Hardware manager +│ │ ├── gpio/ # GPIO pins and interrupts +│ │ ├── timer/ # Timers, PWM, counters +│ │ ├── char/ # UART serial +│ │ ├── spi/ # SPI bus +│ │ ├── i2c/ # I2C bus +│ │ ├── net/ # Network interfaces +│ │ ├── analog/ # ADC/DAC +│ │ ├── intc/ # Interrupt controllers +│ │ ├── flash/ # Flash memory +│ │ ├── dma/ # DMA controllers +│ │ └── ... # Power, SD, misc peripherals +│ │ +│ ├── debugger/ # Debugger subsystem +│ │ ├── gdb/ # GDB remote protocol server +│ │ └── qdb/ # Qiling native debugger (with reverse debugging) +│ │ +│ ├── extensions/ # Optional extensions +│ │ ├── multitask.py # gevent-based multithreading +│ │ ├── trace.py # Instruction tracing +│ │ ├── coverage/ # Code coverage collection +│ │ ├── sanitizers/ # Memory sanitizers +│ │ ├── afl/ # AFL fuzzer integration +│ │ ├── r2/ # Radare2 integration +│ │ └── idaplugin/ # IDA Pro plugin +│ │ +│ └── profiles/ # Default OS configuration files (.ql) +│ ├── linux.ql # Stack/heap addresses, kernel params +│ ├── windows.ql +│ ├── macos.ql +│ └── ... +│ +├── qltool # CLI tool for running binaries +├── qltui.py # TUI interface +├── examples/ # Usage examples and sample scripts +├── tests/ # Test suite +└── docs/ # Documentation +``` + +## Core Components + +### `Qiling` (core.py) + +The central class. Every emulation session creates one `Qiling` instance that owns and wires together all other components: + +```python +ql = Qiling( + argv=["/path/to/binary", "arg1"], # binary + arguments + rootfs="/path/to/rootfs", # virtual filesystem root + ostype=QL_OS.LINUX, # target OS (auto-detected if omitted) + archtype=QL_ARCH.X8664, # target arch (auto-detected if omitted) +) +``` + +`Qiling` inherits from `QlCoreHooks` (hook management) and `QlCoreStructs` (endian-aware packing). Key properties: + +| Property | Type | Description | +|----------|------|-------------| +| `ql.arch` | `QlArch` | CPU architecture — registers, disassembly | +| `ql.os` | `QlOs` | Operating system — syscalls, I/O, APIs | +| `ql.loader` | `QlLoader` | Binary loader — parses and maps the executable | +| `ql.mem` | `QlMemoryManager` | Memory — map, read, write, MMIO | +| `ql.uc` | `unicorn.Uc` | Underlying Unicorn engine instance | + +### Architecture Layer (`qiling/arch/`) + +`QlArch` is the abstract base. Each architecture subclass configures: + +- **Unicorn engine** mode and architecture constants +- **Register** access via `ql.arch.regs` (read/write by name) +- **Disassembler** (Capstone) and **assembler** (Keystone) +- **Stack operations** — push, pop, pointer-width-aware +- **Endianness** and **bit width** (16/32/64) + +Supported: x86, x86-64, 8086, ARM, ARM64, MIPS, RISC-V (32/64), PowerPC, Cortex-M. + +### OS Layer (`qiling/os/`) + +`QlOs` is the abstract base. Each OS subclass provides: + +- **Syscall/interrupt dispatch** — routes CPU interrupts to handler functions +- **I/O streams** — `stdin`, `stdout`, `stderr` (interceptable) +- **Virtual filesystem** — path mapping through `rootfs` +- **Function call interface** (`ql.os.fcall`) — read params, set return values +- **API interception** — `set_api()` for hooking library functions + +**POSIX subsystem** (`os/posix/`): Shared syscall implementation for Linux, FreeBSD, macOS, and QNX. Individual syscall handlers live under `os/posix/syscall/`. + +**Windows** (`os/windows/`): Emulates Win32/Win64 API by hooking DLL imports. Includes registry, thread, handle, and fiber support. + +**UEFI** (`os/uefi/`): Emulates UEFI Boot Services, Runtime Services, and SMM. Uses a GUID database and protocol framework. + +### Loader Layer (`qiling/loader/`) + +`QlLoader` is the abstract base. Loaders parse a binary format, map segments into memory, resolve symbols, load dependencies, and set initial CPU state (PC, SP). + +| Loader | Format | Used By | +|--------|--------|---------| +| `QlLoaderELF` | ELF | Linux, FreeBSD, QNX | +| `QlLoaderPE` | PE/COFF | Windows, UEFI | +| `QlLoaderMacho` | Mach-O | macOS | +| `QlLoaderDOS` | COM/EXE | DOS | +| `QlLoaderMCU` | Firmware | Cortex-M MCU | +| `QlLoaderBlob` | Raw bytes | Shellcode / blob | + +### Memory Manager (`qiling/os/memory.py`) + +Wraps Unicorn's memory model with higher-level operations: + +- `map(addr, size, perms)` / `unmap(addr, size)` — region management +- `read(addr, size)` / `write(addr, data)` — data access +- `read_ptr(addr)` / `write_ptr(addr, val)` — pointer-width-aware access +- `read_cstring(addr)` — null-terminated string read +- MMIO callback support for memory-mapped peripherals + +### Calling Conventions (`qiling/cc/`) + +Each architecture has calling convention classes that abstract argument passing and return values. The `QlOs.fcall` interface uses these to provide a uniform way to read function parameters regardless of platform. + +## Execution Flow + +### 1. Initialization (`Qiling.__init__`) + +``` +Qiling(argv, rootfs) + │ + ├─ Detect arch/OS from binary headers (ql_guess_emu_env) + │ ELF magic → parse e_machine, OSABI + │ PE magic → parse Machine, Subsystem + │ MachO magic → parse CPU type + │ + ├─ Create QlArch (select_arch) → initializes Unicorn engine + ├─ Create QlLoader (select_loader) + ├─ Create QlMemoryManager + ├─ Create QlOs (select_os) + │ + └─ loader.run() + ├─ Parse binary format (headers, segments, sections) + ├─ Map segments into memory + ├─ Load shared libraries / DLLs + ├─ Setup stack, heap, TLS, auxiliary vectors + └─ Set initial PC (entry point) and SP +``` + +### 2. Execution (`ql.run()`) + +``` +ql.run(begin, end, timeout, count) + │ + ├─ Apply binary patches (ql.patch) + ├─ Write exit trap (guard address) + │ + └─ os.run() + └─ uc.emu_start(entry_point, exit_point) + │ + ├─ Unicorn executes instructions + │ + ├─ Hooks fire on: + │ ├─ Every instruction (hook_code) + │ ├─ Basic blocks (hook_block) + │ ├─ Memory access (hook_mem_read/write) + │ ├─ Interrupts (hook_intno) → syscall dispatch + │ ├─ Specific addresses (hook_address) + │ └─ Specific instructions (hook_insn) + │ + └─ Stops when PC reaches exit point, timeout, + ql.emu_stop(), or unhandled exception +``` + +### 3. Syscall Handling + +When the emulated binary issues a syscall (via `int 0x80`, `syscall`, `svc`, etc.): + +``` +CPU interrupt/instruction + → Unicorn interrupt hook + → QlOs syscall dispatcher + → Look up handler by syscall number + → Handler reads args via calling convention + → Emulates syscall behavior + → Sets return value in registers +``` + +## Component Selection + +Components are selected dynamically at runtime based on `QL_ARCH` and `QL_OS` enums. The `qiling/utils.py` module provides: + +- `select_arch(archtype)` → architecture class +- `select_os(ostype)` → OS class +- `select_loader(ostype)` → loader class +- `select_debugger(options)` → debugger class + +This makes it possible to support diverse platform combinations from a unified codebase. + +## Hook System + +The hook system (`core_hooks.py`) wraps Unicorn's callback mechanism: + +| Hook Type | Trigger | +|-----------|---------| +| `hook_code` | Every instruction (optionally within address range) | +| `hook_block` | Every basic block entry | +| `hook_address` | Specific address reached | +| `hook_intno` | CPU interrupt/exception | +| `hook_insn` | Specific instruction type (e.g., `syscall`) | +| `hook_mem_read` | Memory read | +| `hook_mem_write` | Memory write | +| `hook_mem_invalid` | Invalid memory access | + +Hooks can be scoped to address ranges and return `QL_HOOK_BLOCK` to suppress further hooks in the chain. + +## Key Extension Points + +- **Custom syscall handlers** — replace or extend any syscall +- **API hooking** — `ql.os.set_api(name, callback)` to intercept library calls +- **Binary patching** — `ql.patch(offset, data)` for runtime patching +- **State snapshots** — `ql.save()` / `ql.restore()` for checkpointing +- **Debugger attachment** — GDB remote protocol or native QDB debugger +- **Coverage/tracing** — `extensions/coverage/` and `extensions/trace.py` +- **Fuzzing** — AFL integration via `extensions/afl/` +- **Hardware peripherals** — register custom MCU peripherals in `hw/` + +## Dependencies + +| Package | Role | +|---------|------| +| `unicorn` (2.1.3) | CPU emulation engine | +| `capstone` | Disassembly | +| `keystone-engine` | Assembly | +| `pyelftools` | ELF parsing | +| `pefile` | PE parsing | +| `python-registry` | Windows registry emulation | +| `gevent` | Cooperative multithreading | +| `pyyaml` | Configuration parsing | + +Optional: `unicornafl` / `fuzzercorn` (fuzzing), `r2libr` (Radare2 integration). + +## Supported Platforms + +**Architectures:** x86, x86-64, 8086, ARM, ARM64, MIPS, RISC-V (32/64), PowerPC, Cortex-M + +**Operating Systems:** Linux, FreeBSD, macOS, Windows, UEFI, DOS, QNX, MCU (bare-metal), Blob + +## Improvement: Hybrid Kernel Architecture + +> Detailed implementation plan and task tracking: [TODO.md](TODO.md) + +### The Problem + +Qiling reimplements Linux kernel behavior syscall-by-syscall in Python. This works +for simple operations (file I/O, memory management, stat) but fundamentally cannot +scale to the full kernel surface: + +- **Networking**: No epoll. Sockets are proxied to host sockets with no isolation. + No real TCP state machine, no multicast, no raw/netlink sockets. +- **Multithreading**: Gevent greenlets are cooperative and single-threaded. No + preemption, no real concurrency. Futex is a gevent Event. Programs using pthreads, + mutexes, or condition variables don't behave correctly. +- **Signals**: `signal()`, `sigaction()`, `kill()` are mostly stubbed. No delivery, + no `EINTR`, no `SA_RESTART`. +- **Long tail**: capabilities, cgroups, namespaces, io_uring, seccomp, eBPF — the + kernel API surface is vast and growing. + +### The Solution + +A **hybrid architecture** that keeps Unicorn for CPU emulation and Qiling for +instrumentation, but offloads complex kernel subsystems to a real Linux kernel via +a **kernel proxy** helper process. Simple syscalls stay emulated in Python. + +``` +Syscall interrupt + → load_syscall() [UNCHANGED — existing dispatch in posix.py] + → check posix_syscall_hooks[CALL] + → proxy hook registered? → forward to kernel proxy + → no proxy hook? → existing Python handler [UNCHANGED] +``` + +The user explicitly chooses which missing syscalls to forward. Nothing is automatic — +by default Qiling behaves exactly as today. The integration uses the **existing +`set_syscall()` CALL hook mechanism** (`posix.py:128-143`), so `load_syscall()` and +all existing dispatch code remain completely unchanged. + +```python +proxy = KernelProxy(ql) +proxy.forward_syscall("epoll_create", returns_fd=True) +proxy.forward_syscall("epoll_ctl") +proxy.forward_syscall("epoll_wait") +ql.run() +``` + +Under the hood, `forward_syscall()` registers a CALL hook that serializes the +arguments and sends them to a helper process (the kernel proxy) which executes +the real syscall and returns the result. For syscalls that return FDs, the result +is wrapped in a `ql_proxy_fd` object and stored in Qiling's FD table. Since the +FD table is already polymorphic (`ql_socket`, `ql_file`, `ql_pipe`), existing +handlers like `ql_syscall_read` and `ql_syscall_close` dispatch through the proxy +FD's `.read()`/`.close()` methods automatically — no changes needed. + +### Phases + +| Phase | Scope | Risk | Goal | +|-------|-------|------|------| +| 0 | Proof of concept | Low | User manually forwards specific syscalls — zero existing code changed | +| 1 | Networking foundation | Low-Med | Specific hooks for socket syscalls, `ql_proxy_fd`, TCP works | +| 2 | Complete networking | Medium | epoll, poll/select, network namespaces | +| 3 | Real threading | **High** | One Unicorn per thread, shared memory, real futex | +| 4 | Signals | Medium | Real signal delivery, EINTR, handler execution | +| 5 | Integration | Low | API polish, fallback, platform support, benchmarks | + +Phase 0 gives users explicit control — they identify which missing syscalls to +forward and the proxy handles them. Phases 1-2 add pointer-aware forwarding for +networking with pre-built forwarders so users don't have to wire up each syscall. +Phase 3 (threading) is the highest-risk change and is deferred until networking is +stable. Each phase preserves backward compatibility — hybrid mode is opt-in, default +behavior is unchanged. + +### Alternatives Considered + +- **Run a real kernel in Unicorn**: Unicorn doesn't emulate hardware (interrupt + controllers, MMU page tables, timers). Would require rebuilding QEMU system mode. +- **ptrace-based execution**: Run natively, intercept syscalls. Fast, but no + cross-architecture support and limited instruction-level hooks. +- **User-Mode Linux (UML)**: Run the kernel as a userspace process. x86-only, + somewhat unmaintained, complex syscall bridge. +- **Auto-forward all unimplemented syscalls**: Forward every missing syscall + automatically. Convenient but unpredictable — hard to debug, may forward syscalls + that shouldn't be (security, state leaks). Explicit user control is safer. + +The hybrid approach was chosen because it preserves Qiling's core value +(instrumentation + cross-arch emulation) while getting real kernel behavior where +it matters most — without modifying the existing dispatch path. + +## Testing + +Tests live in `tests/` and are organized by platform: `test_elf.py`, `test_pe.py`, `test_macho.py`, `test_dos.py`, `test_mcu.py`, etc. They use binaries from `examples/rootfs/` as test fixtures. diff --git a/README.md b/README.md index 34a02ef68..05d6945b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Documentation Status](https://github.com/qilingframework/qiling/wiki)](https://github.com/qilingframework/qiling/wiki) +[![Documentation Status](https://readthedocs.org/projects/qilingframework/badge/?version=latest)](https://docs.qiling.io) [![Downloads](https://pepy.tech/badge/qiling)](https://pepy.tech/project/qiling) [![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/qilingframework) @@ -8,43 +8,24 @@

-# Qiling Framework - -Qiling is an advanced binary emulation framework that allows you to emulate and sandbox code in an isolated environment across multiple platforms and architectures. Built on top of Unicorn Engine, Qiling provides a higher-level framework that understands operating system contexts, executable formats, and dynamic linking. - -## Table of Contents - -- [Features](#features) -- [Appearance](#Appearance) -- [Use Cases](#use-cases) -- [Quick Start](#quick-start) - - [Installation](#installation) - - [Basic Usage](#basic-usage) -- [Qiling vs. Other Emulators](#qiling-vs-other-emulators) - - [Qiling vs. Unicorn Engine](#qiling-vs-unicorn-engine) - - [Qiling vs. QEMU User Mode](#qiling-vs-qemu-user-mode) -- [Examples](#examples) -- [Qltool](#qltool) -- [Contributing](#contributing) -- [License](#license) -- [Contact](#contact) -- [Core Developers & Contributors](#core-developers--contributors) - -## Features - -- **Multi-platform Emulation**: Windows, macOS, Linux, Android, BSD, UEFI, DOS, MBR. -- **Multi-architecture Emulation**: 8086, X86, X86_64, ARM, ARM64, MIPS, RISC-V, PowerPC. -- **Multiple File Format Support**: PE, Mach-O, ELF, COM, MBR. -- **Kernel Module Emulation**: Supports Windows Driver (.sys), Linux Kernel Module (.ko) & macOS Kernel (.kext) via [Demigod](https://groundx.io/demigod/). -- **Isolated Sandboxing**: Emulates & sandboxes code in an isolated environment with a fully configurable sandbox. -- **In-depth API**: Provides in-depth memory, register, OS level, and filesystem level API. -- **Fine-grain Instrumentation**: Allows hooks at various levels (instruction/basic-block/memory-access/exception/syscall/IO/etc.). -- **Virtual Machine Level API**: Supports saving and restoring the current execution state. -- **Debugging Capabilities**: Supports cross-architecture and platform debugging, including a built-in debugger with reverse debugging capability. -- **Dynamic Hot Patching**: Allows dynamic hot patching of on-the-fly running code, including loaded libraries. -- **Python Framework**: A true framework in Python, making it easy to build customized security analysis tools. - -## Appearance +[Qiling's use case, blog and related work](https://github.com/qilingframework/qiling/issues/134) + +Qiling is an advanced binary emulation framework, with the following features: + +- Emulate multi-platforms: Windows, macOS, Linux, Android, BSD, UEFI, DOS, MBR. +- Emulate multi-architectures: 8086, X86, X86_64, ARM, ARM64, MIPS, RISC-V, PowerPC. +- Support multiple file formats: PE, Mach-O, ELF, COM, MBR. +- Support Windows Driver (.sys), Linux Kernel Module (.ko) & macOS Kernel (.kext) via [Demigod](https://groundx.io/demigod/). +- Emulates & sandbox code in an isolated environment. +- Provides a fully configurable sandbox. +- Provides in-depth memory, register, OS level and filesystem level API. +- Fine-grain instrumentation: allows hooks at various levels + (instruction/basic-block/memory-access/exception/syscall/IO/etc.) +- Provides virtual machine level API such as saving and restoring the current execution state. +- Supports cross architecture and platform debugging capabilities. +- Built-in debugger with reverse debugging capability. +- Allows dynamic hot patch on-the-fly running code, including the loaded library. +- True framework in Python, making it easy to build customized security analysis tools on top. Qiling also made its way to various international conferences. @@ -68,37 +49,79 @@ Qiling also made its way to various international conferences. - [Nullcon](https://nullcon.net/website/goa-2020/speakers/kaijern-lau.php) 2019: + - [DEFCON, USA](https://www.defcon.org/html/defcon-27/dc-27-demolabs.html#QiLing) - [Hitcon](https://hitcon.org/2019/CMT/agenda) - [Zeronights](https://zeronights.ru/report-en/qiling-io-advanced-binary-emulation-framework/) -## Use Cases -Qiling has been presented at various international conferences, showcasing its versatility in: +Qiling is backed by [Unicorn Engine](http://www.unicorn-engine.org). -- Binary analysis and reverse engineering. -- Malware analysis and sandboxing. -- Firmware analysis and emulation. -- Security research and vulnerability discovery. -- CTF challenges and exploit development. +Visit our [website](https://www.qiling.io) for more information. -For more details on Qiling's use cases, blog posts, and related work, please refer to [Qiling's use case, blog and related work](https://github.com/qilingframework/qiling/issues/134). +--- +#### License -## Quick Start +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. -### Installation +--- -Qiling requires Python 3.8 or newer. You can install it using pip: +#### Qiling vs. other Emulators -```bash -pip install qiling -``` +There are many open-source emulators, but two projects closest to Qiling +are [Unicorn](http://www.unicorn-engine.org) & [QEMU user mode](https://qemu.org). +This section explains the main differences of Qiling against them. + +##### Qiling vs. Unicorn engine + +Built on top of Unicorn, but Qiling & Unicorn are two different animals. + +- Unicorn is just a CPU emulator, so it focuses on emulating CPU instructions, + that can understand emulator memory. + Beyond that, Unicorn is not aware of higher level concepts, such as dynamic + libraries, system calls, I/O handling or executable formats like PE, Mach-O + or ELF. As a result, Unicorn can only emulate raw machine instructions, + without Operating System (OS) context. +- Qiling is designed as a higher level framework, that leverages Unicorn to + emulate CPU instructions, but can understand OS: it has executable format + loaders (for PE, Mach-O & ELF currently), dynamic linkers (so we can + load & relocate shared libraries), syscall & IO handlers. For this reason, + Qiling can run executable binary without requiring its native OS. + +##### Qiling vs. QEMU user mode + +QEMU user mode does a similar thing to our emulator, that is, to emulate whole +executable binaries in a cross-architecture way. +However, Qiling offers some important differences against QEMU user mode: + +- Qiling is a true analysis framework, + that allows you to build your own dynamic analysis tools on top (in Python). + Meanwhile, QEMU is just a tool, not a framework. +- Qiling can perform dynamic instrumentation, and can even hot patch code at + runtime. QEMU does neither. +- Not only working cross-architecture, Qiling is also cross-platform. + For example, you can run Linux ELF file on top of Windows. + In contrast, QEMU user mode only runs binary of the same OS, such as Linux + ELF on Linux, due to the way it forwards syscall from emulated code to + native OS. +- Qiling supports more platforms, including Windows, macOS, Linux & BSD. QEMU + user mode can only handle Linux & BSD. -For more detailed installation instructions and dependencies, please refer to the [official documentation](https://github.com/qilingframework/qiling/wiki/Installation). +--- + +#### Installation + +Please see [setup guide](https://github.com/qilingframework/qiling/wiki/Installation) file for how to install Qiling Framework. + +--- -### Basic Usage +#### Examples -The example below shows how to use Qiling framework in the most straightforward way to emulate a Windows executable. +The example below shows how to use Qiling framework in the most +straightforward way to emulate a Windows executable. ```python from qiling import Qiling @@ -112,30 +135,8 @@ if __name__ == "__main__": ql.run() ``` -## Qiling vs. Other Emulators - -There are many open-source emulators, but two projects closest to Qiling are [Unicorn](http://www.unicorn-engine.org) & [QEMU user mode](https://qemu.org). This section explains the main differences of Qiling against them. - -### Qiling vs. Unicorn Engine - -Built on top of Unicorn, but Qiling & Unicorn are two different animals. - -- **Unicorn** is just a CPU emulator, so it focuses on emulating CPU instructions, that can understand emulator memory. Beyond that, Unicorn is not aware of higher level concepts, such as dynamic libraries, system calls, I/O handling or executable formats like PE, Mach-O or ELF. As a result, Unicorn can only emulate raw machine instructions, without Operating System (OS) context. -- **Qiling** is designed as a higher level framework, that leverages Unicorn to emulate CPU instructions, but can understand OS: it has executable format loaders (for PE, Mach-O & ELF currently), dynamic linkers (so we can load & relocate shared libraries), syscall & IO handlers. For this reason, Qiling can run executable binary without requiring its native OS. - -### Qiling vs. QEMU User Mode - -QEMU user mode does a similar thing to our emulator, that is, to emulate whole executable binaries in a cross-architecture way. -However, Qiling offers some important differences against QEMU user mode: - -- **Qiling is a true analysis framework**, that allows you to build your own dynamic analysis tools on top (in Python). Meanwhile, QEMU is just a tool, not a framework. -- **Qiling can perform dynamic instrumentation**, and can even hot patch code at runtime. QEMU does neither. -- Not only working cross-architecture, **Qiling is also cross-platform**. For example, you can run Linux ELF file on top of Windows. In contrast, QEMU user mode only runs binary of the same OS, such as Linux ELF on Linux, due to the way it forwards syscall from emulated code to native OS. -- **Qiling supports more platforms**, including Windows, macOS, Linux & BSD. QEMU user mode can only handle Linux & BSD. - -## Examples - -- The following example shows how a Windows crackme may be patched dynamically to make it always display the “Congratulation” dialog. +- The following example shows how a Windows crackme may be patched dynamically + to make it always display the “Congratulation” dialog. ```python from qiling import Qiling @@ -176,13 +177,15 @@ The below YouTube video shows how the above example works. #### Emulating ARM router firmware on Ubuntu x64 host -Qiling Framework hot-patches and emulates an ARM router's `/usr/bin/httpd` on an x86_64 Ubuntu host. +Qiling Framework hot-patches and emulates an ARM router's `/usr/bin/httpd` on +an x86_64 Ubuntu host. -[![Qiling Tutorial: Emulating and Fuzz ARM router firmware](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/fuzzer.jpg?raw=true)](https://www.youtube.com/watch?v=e3_T3KLhNUs) +[![Qiling Tutorial: Emulating and Fuzz ARM router firmware](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/fuzzer.jpg?raw=true)](https://www.youtube.com/watch?v=e3_T3KLh2NU) #### Qiling's IDA Pro Plugin: Instrument and Decrypt Mirai's Secret -This video demonstrates how Qiling's IDA Pro plugin can make IDA Pro run with Qiling instrumentation engine. +This video demonstrates how Qiling's IDA Pro plugin can make IDA Pro run with +Qiling instrumentation engine. [![Qiling's IDA Pro Plugin: Instrument and Decrypt Mirai's Secret](http://img.youtube.com/vi/ZWMWTq2WTXk/0.jpg)](http://www.youtube.com/watch?v=ZWMWTq2WTXk) @@ -192,62 +195,63 @@ Solving a simple CTF challenge with Qiling Framework and IDA Pro [![Solving a simple CTF challenge with Qiling Framework and IDA Pro](https://i.ytimg.com/vi/SPjVAt2FkKA/0.jpg)](https://www.youtube.com/watch?v=SPjVAt2FkKA) + #### Emulating MBR Qiling Framework emulates MBR [![Qiling DEMO: Emulating MBR](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/mbr.png?raw=true)](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/mbr.png?raw=true) -## Qltool +--- + +#### Qltool Qiling also provides a friendly tool named `qltool` to quickly emulate shellcode & executable binaries. With qltool, easy execution can be performed: + With shellcode: -```bash +``` $ ./qltool code --os linux --arch arm --format hex -f examples/shellcodes/linarm32_tcp_reverse_shell.hex ``` With binary file: -```bash +``` $ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs examples/rootfs/x8664_linux/ ``` With binary and GDB debugger enabled: -```bash +``` $ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:9999 --rootfs examples/rootfs/x8664_linux ``` With code coverage collection (UEFI only for now): -```bash +``` $ ./qltool run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --coverage-format drcov --coverage-file TcgPlatformSetupPolicy.cov ``` With JSON output (Windows, mainly): -```bash +``` $ ./qltool run -f examples/rootfs/x86_windows/bin/x86_hello.exe --rootfs examples/rootfs/x86_windows/ --console False --json ``` +--- -## Contributing - -We welcome contributions from the community! If you're interested in contributing to Qiling Framework, please check out our [GitHub repository](https://github.com/qilingframework/qiling) and look for open issues or submit a pull request. - -## License -This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. +#### Contact -## Contact +Get the latest info from our website https://www.qiling.io -Get the latest info from our website [https://www.qiling.io](https://www.qiling.io) +Contact us at email info@qiling.io, +via Twitter [@qiling_io](https://twitter.com/qiling_io). -Contact us at email [info@qiling.io](mailto:info@qiling.io), or via Twitter [@qiling_io](https://twitter.com/qiling_io). +--- -## Core Developers & Contributors +#### Core developers, Key Contributors and etc. -Please refer to [CREDITS.md](https://github.com/qilingframework/qiling/blob/dev/CREDITS.md). \ No newline at end of file +Please refer to [CREDITS.md](https://github.com/qilingframework/qiling/blob/dev/CREDITS.md). diff --git a/TODO b/TODO deleted file mode 100644 index b2471eb14..000000000 --- a/TODO +++ /dev/null @@ -1 +0,0 @@ -Features request and TODO please refer to issue 333 https://github.com/qilingframework/qiling/issues/333 diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..449b7df5d --- /dev/null +++ b/TODO.md @@ -0,0 +1,693 @@ +# Qiling Framework TODO + +Features request and TODO please refer to issue 333 https://github.com/qilingframework/qiling/issues/333 + +--- + +## Hybrid Kernel Architecture + +### The Problem + +Qiling reimplements Linux kernel behavior syscall-by-syscall in Python. Simple +syscalls (file I/O, memory, stat) work well. Complex subsystems do not: + +- **Networking**: No epoll. Sockets are host sockets with no isolation. No TCP + state machine, no multicast, no raw/netlink sockets. +- **Threading**: Gevent greenlets — cooperative, single-threaded. No preemption. + Futex is a gevent Event. pthreads don't behave correctly. +- **Signals**: `signal()`, `sigaction()`, `kill()` are stubs. No delivery, no EINTR. + +Reimplementing the full kernel is not realistic. Instead, offload complex subsystems +to a real Linux kernel while keeping Qiling's instrumentation intact. + +### Key Insight: Two-Layer Forwarding + +There are two integration points, each serving a different purpose: + +**Layer 1 — Generic fallback** (catches the long tail): + +The user explicitly chooses which missing syscalls to forward. Nothing is automatic. +By default Qiling behaves exactly as it does today — if a syscall is not implemented, +it fails. The user then tells the proxy which specific syscalls to forward: + +```python +ql = Qiling(argv=["/bin/myserver"], rootfs="rootfs/x8664_linux") +proxy = KernelProxy(ql) # start proxy process +proxy.forward_syscall("epoll_create") # forward this one to real kernel +proxy.forward_syscall("epoll_ctl") # and this one +proxy.forward_syscall("epoll_wait") # and this one +ql.run() +``` + +Under the hood, `forward_syscall("epoll_create")` registers a CALL hook via the +existing `set_syscall()` mechanism (`posix.py:128-143`). The hook reads args from +the emulated registers, sends them to the proxy process, and returns the real result. +**Zero changes to `load_syscall()` or any existing dispatch code.** + +For syscalls that return FDs (socket, epoll_create, eventfd, etc.), the proxy +wraps the returned FD in a `ql_proxy_fd` object and stores it in the FD table. +The FD table (`QlFileDes`) already stores polymorphic objects — `ql_socket`, +`ql_file`, `ql_pipe` — so `ql_proxy_fd` slots in naturally. When existing handlers +like `ql_syscall_read` or `ql_syscall_close` hit a proxy FD, they dispatch through +`ql_proxy_fd.read()` / `.close()` which forwards to the proxy. + +``` +Syscall interrupt + → load_syscall() [UNCHANGED] + → has CALL hook? (user hook or proxy-registered hook) + → yes: use it + → has Python handler? (existing code) + → yes: use it [unchanged — file I/O, memory, stat, etc.] + → neither? + → log warning [existing behavior, unchanged — no auto-forwarding] +``` + +### What Changes vs What Doesn't + +| Component | Changes? | Notes | +|-----------|----------|-------| +| `load_syscall()` dispatch | **NO** | Entirely unchanged | +| Existing syscall handlers | NO | Python handlers stay as-is | +| `QlFileDes` FD table | NO | Already polymorphic, new FD type slots in | +| `set_syscall()` user hooks | NO | User CALL/ENTER/EXIT hooks still work | +| `ql.run()` / `ql.emu_start()` | NO | Unicorn execution loop untouched | +| Hook system (`core_hooks.py`) | NO | Standard Unicorn hook mechanism | +| New: kernel proxy process | YES | New module, new files only | +| New: `ql_proxy_fd` FD type | YES | New class, same interface as `ql_socket` | +| New: `KernelProxy` class | YES | User-facing API, registers CALL hooks | + +--- + +## Phase 0: Proof of Concept + +**Goal**: User can explicitly forward specific unimplemented syscalls to a real Linux +kernel. No automatic behavior. No changes to existing Qiling dispatch code. + +### Usage + +```python +from qiling import Qiling +from qiling.os.posix.kernel_proxy import KernelProxy + +ql = Qiling(argv=["/bin/myserver"], rootfs="rootfs/x8664_linux") + +# Start a kernel proxy — a helper process that executes real syscalls +proxy = KernelProxy(ql) + +# Binary needs epoll but Qiling doesn't implement it. +# User identifies the 3 missing syscalls and forwards them: +proxy.forward_syscall("epoll_create") +proxy.forward_syscall("epoll_ctl") +proxy.forward_syscall("epoll_wait") + +ql.run() +# epoll_create/ctl/wait are handled by real kernel. +# Everything else (read, write, open, mmap, ...) uses existing Qiling handlers. +``` + +If the user does NOT set up a proxy, Qiling behaves exactly as it does today. +No surprises, no magic. + +### 0.1 Kernel proxy process + +A standalone Python process that executes real Linux syscalls on behalf of Qiling. +Communicates via Unix socketpair. + +- [ ] New directory: `qiling/os/posix/kernel_proxy/` +- [ ] `proxy.py` — proxy subprocess entry point + - Main loop: read request from socket → `libc.syscall(nr, *args)` → write response + - Uses `ctypes.CDLL("libc.so.6").syscall()` for raw syscall execution + - Manages its own FD table (proxy-side FDs) +- [ ] IPC protocol (binary, over socketpair): + ``` + Request: { type: SYSCALL, syscall_nr: u32, args: [u64; 6] } + Response: { return_value: i64, errno: i32 } + + Request: { type: FD_OP, op: READ|WRITE|CLOSE, proxy_fd: i32, length: u32, data?: bytes } + Response: { return_value: i64, errno: i32, data?: bytes } + ``` + Two message types: raw syscall forwarding, and FD operations (for read/write/close + on proxy-owned FDs). +- [ ] Lifecycle: started by `KernelProxy.__init__()`, killed on `ql.run()` exit + +### 0.2 `KernelProxy` class — user-facing API + +The main integration class. Lives in `qiling/os/posix/kernel_proxy/__init__.py`. + +```python +class KernelProxy: + def __init__(self, ql: Qiling): + """Start the proxy subprocess.""" + self.ql = ql + self._proxy_process = ... # start subprocess + self._ipc = ... # socketpair connection + self._forwarded = {} # syscall_name → syscall_nr + + def forward_syscall(self, name: str, returns_fd: bool = False): + """Register a CALL hook that forwards this syscall to the proxy. + + Args: + name: syscall name (e.g. "epoll_create", "eventfd") + returns_fd: if True, wrap the return value in ql_proxy_fd + and store in the FD table + """ + # Look up syscall number from the architecture's syscall table + # Register a CALL hook via ql.os.set_syscall() + ql.os.set_syscall(name, self._make_forwarder(name, returns_fd)) + + def _make_forwarder(self, name, returns_fd): + """Create a CALL hook function for this syscall.""" + syscall_nr = self._resolve_syscall_nr(name) + + def forwarder(ql, *args): + # Send all args as raw integers to proxy + retval = self._ipc.forward(syscall_nr, args) + + if returns_fd and retval >= 0: + # Wrap proxy FD and store in Qiling's FD table + proxy_fd = ql_proxy_fd(self._ipc, retval) + guest_fd = self._alloc_fd(ql, proxy_fd) + return guest_fd + + return retval + + return forwarder +``` + +Key points: +- `forward_syscall()` uses the existing `set_syscall()` mechanism — a standard + CALL hook. No special dispatch path, no changes to `load_syscall()`. +- User explicitly passes `returns_fd=True` for FD-returning syscalls. No heuristics. +- User ENTER/EXIT hooks still fire around the forwarded syscall (existing behavior + of the hook chain in `load_syscall()` lines 206-224). + +- [ ] Implement `KernelProxy` class +- [ ] Implement `_resolve_syscall_nr()` — look up syscall number from arch tables +- [ ] Implement `_make_forwarder()` — create CALL hook closure +- [ ] Implement `_alloc_fd()` — find empty slot in `ql.os.fd[]`, store `ql_proxy_fd` + +### 0.3 `ql_proxy_fd` — proxy-side FD wrapper + +When a forwarded syscall returns an FD (e.g., `epoll_create` returns 5 in the proxy), +we store a `ql_proxy_fd` in Qiling's FD table. This object forwards read/write/close +to the proxy, matching the interface of `ql_socket` (`filestruct.py:14`). + +```python +class ql_proxy_fd: + """FD whose real file/socket lives in the kernel proxy process.""" + + def __init__(self, ipc, proxy_fd: int): + self._ipc = ipc + self._proxy_fd = proxy_fd + + def read(self, length: int) -> bytes: + return self._ipc.fd_read(self._proxy_fd, length) + + def write(self, data: bytes) -> int: + return self._ipc.fd_write(self._proxy_fd, data) + + def close(self) -> None: + self._ipc.fd_close(self._proxy_fd) + + def fileno(self) -> int: + return -1 # not a real host FD +``` + +Because `ql_syscall_read` / `ql_syscall_write` / `ql_syscall_close` already dispatch +through `ql.os.fd[fd].read()` / `.write()` / `.close()`, **these existing handlers +need no changes**. When the binary calls `read(fd, buf, n)` on a proxy FD, the +existing read handler calls `ql_proxy_fd.read(n)`, gets data back, and writes it +to guest memory as usual. + +- [ ] Implement `ql_proxy_fd` class in `qiling/os/posix/kernel_proxy/proxy_fd.py` +- [ ] Verify `ql_syscall_read` works with `ql_proxy_fd` — no changes needed +- [ ] Verify `ql_syscall_write` works with `ql_proxy_fd` — no changes needed +- [ ] Verify `ql_syscall_close` works with `ql_proxy_fd` — no changes needed + +### 0.4 Pointer-bearing syscalls (Phase 0 scope: one example) + +Some forwarded syscalls take pointers to guest memory. The proxy can't read guest +memory directly. For Phase 0, implement ONE pointer-bearing forwarder as an example +to prove the pattern works. `epoll_ctl` is a good candidate: + +``` +epoll_ctl(epfd, op, fd, struct epoll_event *event) +``` + +The forwarder must: +1. Read `struct epoll_event` (8 bytes) from guest memory at the `event` pointer +2. Send the struct data along with the integer args to the proxy +3. Proxy reconstructs the struct in its own memory, calls real `epoll_ctl` + +- [ ] Extend IPC protocol for buffer-carrying requests: + ``` + Request: { type: SYSCALL_WITH_BUFS, syscall_nr, args[6], buffers: [(arg_idx, direction, data)] } + Response: { return_value, errno, buffers: [(arg_idx, data)] } + ``` + `direction` is IN (guest→proxy), OUT (proxy→guest), or INOUT. +- [ ] Implement `forward_syscall_with_buffers()` API for pointer-bearing syscalls +- [ ] Implement `epoll_ctl` forwarder as the working example + +### 0.5 Validation + +- [ ] Test: binary that uses `epoll_create` + `epoll_ctl` + `epoll_wait` on a + timerfd or eventfd. User forwards all 4 syscalls. Verify it works end-to-end. +- [ ] Test: same binary WITHOUT proxy — Qiling fails as it does today. No regression. +- [ ] Test: binary that calls `socket()` — existing Qiling handler runs (user did + NOT forward `socket`). Verify no interference. +- [ ] Test: user `set_syscall("epoll_create", my_hook)` — user hook takes priority + over proxy hook (user hook registered after proxy hook overwrites it via + `set_syscall`). Verify user control is preserved. +- [ ] Test: proxy process crash — verify Qiling reports error cleanly, doesn't hang. + +**Existing code modified**: NONE. All new files in `qiling/os/posix/kernel_proxy/`. +Integration is purely through `set_syscall()`. + +**Risk**: LOW — new code in new module. Existing behavior completely unchanged unless +user explicitly creates a `KernelProxy` and calls `forward_syscall()`. + +--- + +## Phase 1: Networking Foundation + +**Goal**: Forward all socket syscalls. Make a TCP client work end-to-end. + +### 1.1 `ql_proxy_socket` FD type + +New class in `qiling/os/posix/filestruct.py` (or new file alongside it). Must match +`ql_socket` interface so generic I/O dispatches correctly: + +```python +class ql_proxy_socket: + """Socket FD whose real socket lives in the kernel proxy process.""" + + def read(self, length: int) -> bytes: + # Forward to proxy: recv(self.proxy_fd, length) + + def write(self, data: bytes) -> int: + # Forward to proxy: send(self.proxy_fd, data) + + def close(self) -> None: + # Forward to proxy: close(self.proxy_fd) + + def fileno(self) -> int: + # Return a sentinel — not a real host FD + + # Socket-specific methods forwarded to proxy: + def connect(self, address) -> None: ... + def bind(self, address) -> None: ... + def listen(self, backlog) -> None: ... + def accept(self) -> tuple: ... + def shutdown(self, how) -> None: ... + def setsockopt(self, level, optname, value) -> None: ... + def getsockopt(self, level, optname) -> ...: ... +``` + +Because `ql_syscall_read` / `ql_syscall_write` / `ql_syscall_close` already dispatch +through `ql.os.fd[fd].read()` / `.write()` / `.close()`, these **existing handlers +need no changes** — the proxy socket object handles forwarding internally. + +- [ ] Implement `ql_proxy_socket` class +- [ ] IPC client method for each operation +- [ ] Verify generic `read(fd, ...)` and `write(fd, ...)` work on proxy sockets + without modifying `ql_syscall_read` or `ql_syscall_write` + +### 1.2 Socket syscall CALL hooks + +Register CALL hooks for socket-specific syscalls. These are needed because socket +syscalls (bind, connect, listen, accept, etc.) have special argument handling +(sockaddr structs, address lengths) that goes beyond generic read/write. + +- [ ] `socket()` — create proxy socket, store `ql_proxy_socket` in FD table +- [ ] `bind(fd, addr, addrlen)` — read sockaddr from guest memory, forward to proxy +- [ ] `connect(fd, addr, addrlen)` — same pattern +- [ ] `listen(fd, backlog)` — forward +- [ ] `accept(fd, addr, addrlen)` — forward, create new `ql_proxy_socket` for client FD +- [ ] `send/sendto/sendmsg` — read buffer from guest memory, forward +- [ ] `recv/recvfrom/recvmsg` — forward, write received data to guest memory +- [ ] `setsockopt/getsockopt` — forward with option translation +- [ ] `getpeername/getsockname` — forward, write sockaddr to guest memory +- [ ] `shutdown` — forward +- [ ] `socketpair` — forward, create two `ql_proxy_socket` objects +- [ ] `close` on proxy sockets — handled by `ql_proxy_socket.close()`, + but also register hook to detect close on proxy FDs if needed + +**Struct translation**: sockaddr family (AF_INET, AF_INET6, AF_UNIX) is the same +across architectures. Network byte order is architecture-independent. The main +concern is pointer width (32-bit guest on 64-bit host) — read the right number +of bytes from guest memory based on `ql.arch.pointersize`. + +### 1.3 `socketcall()` multiplexer (x86 32-bit) + +On x86 32-bit, all socket operations go through a single `socketcall()` syscall +(`qiling/os/posix/syscall/net.py`). The existing multiplexer dispatches to individual +handlers. Since we hook the individual handlers (bind, connect, etc.), this works +automatically. But verify: + +- [ ] Test that x86 32-bit socket operations are correctly forwarded via the + existing socketcall → individual handler → our CALL hook chain + +### 1.4 Validation + +- [ ] TCP client: connect to a server, send/receive data, close +- [ ] TCP server: bind, listen, accept, handle client, close +- [ ] UDP: sendto/recvfrom +- [ ] Unix domain sockets (path-based) +- [ ] Existing non-network tests still pass (regression check) + +**Risk**: LOW-MEDIUM — new code + new FD type, but existing handlers and dispatch +untouched. Main risk is FD lifecycle bugs (leak, double-close). + +--- + +## Phase 2: Complete Networking + +**Goal**: epoll, network namespaces, advanced operations. Real-world network +binaries work. + +### 2.1 epoll forwarding + +epoll is currently **not implemented at all** — mapped in the syscall table but +no handler. This is new functionality, not a change to existing behavior. + +- [ ] `epoll_create` / `epoll_create1` — forward, return proxy epoll FD + (new FD type or reuse `ql_proxy_socket` with a flag) +- [ ] `epoll_ctl(epfd, op, fd, event)` — forward; `fd` must be translated to + proxy FD space. Read `epoll_event` struct from guest memory. +- [ ] `epoll_wait(epfd, events, maxevents, timeout)` — forward. **This blocks** + in the proxy. For single-threaded programs this is correct (binary would be + blocked anyway). Write returned events to guest memory. +- [ ] `epoll_pwait` — same as epoll_wait + signal mask + +**Blocking concern**: when the proxy is blocked on `epoll_wait`, the Unicorn +emulation is paused. This is correct for single-threaded programs. For multithreaded +programs, we need real threading (Phase 3) where each thread has its own Unicorn +and can block independently. + +### 2.2 poll/select integration + +Currently `poll()` and `select()` use host `select.poll()`/`select.select()` directly, +which won't work for proxy FDs (no host FD to poll). + +- [ ] Hook `poll()` and `select()` — for FD sets containing proxy FDs, forward the + entire operation to the proxy +- [ ] For mixed FD sets (some proxy, some local): forward the proxy FDs to the proxy, + poll local FDs locally, merge results. This is complex — consider forwarding all + FDs to the proxy as the simpler approach. + +### 2.3 Network namespace isolation + +- [ ] Proxy process runs in its own network namespace (`unshare(CLONE_NEWNET)`) +- [ ] Configurable modes: + - `host`: proxy shares host network (default, simplest) + - `isolated`: separate namespace, no connectivity + - `bridged`: veth pair with NAT to host +- [ ] DNS: mount a resolv.conf in the proxy's mount namespace if needed + +### 2.4 Advanced operations (lower priority) + +- [ ] `sendmmsg` / `recvmmsg` — batch send/receive +- [ ] Raw sockets / packet sockets (`AF_PACKET`) +- [ ] Netlink sockets (`AF_NETLINK`) — for binaries that call `ip`, `route`, etc. +- [ ] `SCM_RIGHTS` (FD passing over Unix sockets) — requires FD translation +- [ ] IPv6 multicast + +### 2.5 Validation + +- [ ] epoll-based TCP echo server +- [ ] HTTP client (wget/curl-like binary) +- [ ] Binary that uses poll() with mixed file + socket FDs +- [ ] Network namespace: verify proxy and emulated binary are isolated from host +- [ ] Performance: measure latency overhead of IPC per syscall + +**Risk**: MEDIUM — epoll is new functionality (no regression risk), but poll/select +changes for proxy FDs touch existing handlers. The mixed-FD-set case is the main +complexity. + +--- + +## Phase 3: Real Threading + +**Goal**: Real concurrency with one Unicorn engine per thread. + +**This phase is high-risk and should only start after Phase 2 is stable.** It touches +the Unicorn integration, memory manager, thread lifecycle, and scheduler — all core +components. Needs a detailed design document before implementation begins. + +### Prerequisites + +- [ ] Phase 1-2 networking is stable and tested +- [ ] Detailed design document covering memory sharing, thread lifecycle, + and failure modes +- [ ] Prototype benchmark: measure overhead of multiple Unicorn instances + sharing memory via `mem_map_ptr` + +### 3.1 Shared memory backing for Unicorn + +Currently `QlMemoryManager.map()` calls `uc.mem_map()` which allocates internal +Unicorn memory. For shared threading, all Unicorn instances must see the same memory. + +- [ ] Change memory backing to use `mmap(MAP_SHARED)` + `uc.mem_map_ptr()` +- [ ] This affects: `QlMemoryManager.map()`, `QlMemoryManager.protect()`, + `QlMemoryManager.unmap()` +- [ ] Loader changes: ELF/PE/MachO loaders must write segments into shared-backed + memory regions +- [ ] MMIO regions stay callback-based (not shared) +- [ ] **Critical**: This must be done as a standalone change that passes ALL existing + tests before moving to 3.2. If existing tests break, the shared memory + implementation is wrong. + +Files: `qiling/os/memory.py`, `qiling/loader/elf.py`, `qiling/loader/pe.py` + +### 3.2 One Unicorn instance per thread + +Replace gevent Greenlets with real OS threads, each owning a Unicorn instance. + +- [ ] New thread class: `QlLinuxRealThread` (alongside existing `QlLinuxThread`) + - Creates a new `Uc` instance on spawn + - Maps all shared memory regions into the new Uc via `mem_map_ptr` + - Copies parent registers to child Uc + - Sets child's SP, TLS, return value + - Runs in a real `threading.Thread` +- [ ] Modify `clone()` handler: when hybrid threading is enabled, create + `QlLinuxRealThread` instead of gevent Greenlet +- [ ] Per-thread hook context: each Unicorn instance needs its own hooks registered. + User-defined hooks must be replicated to all instances. +- [ ] Remove the 32337-instruction cooperative scheduling loop — real OS scheduler + handles preemption + +**What breaks**: The current model assumes ONE `ql.uc` instance. Code that accesses +`ql.uc` directly will see only one thread's Unicorn. Need to audit all `ql.uc` +references and route to the current thread's instance. + +Risky references: +- `ql.arch.regs` reads/writes `ql.uc` registers — must route to current thread's Uc +- `ql.mem.read/write` calls `ql.uc.mem_read/write` — with shared memory, any Uc works +- `ql.hook_*` registers on `ql.uc` — must register on all Uc instances +- `ql.save()/restore()` snapshots `ql.uc` — must snapshot correct thread + +### 3.3 Synchronization primitives via real kernel + +With real OS threads sharing real memory, kernel synchronization works natively. + +- [ ] Forward `futex()` to kernel — `FUTEX_WAIT`/`FUTEX_WAKE` operate on the shared + memory addresses directly +- [ ] Remove gevent Event-based futex emulation (`qiling/os/linux/futex.py`) +- [ ] Forward `set_robust_list`, `get_robust_list` +- [ ] `pthread_mutex_*`, `pthread_cond_*` — these use futex internally, so forwarding + futex is sufficient + +### 3.4 Thread safety for shared state + +With real concurrent threads, shared mutable state needs synchronization. + +- [ ] `QlMemoryManager`: lock `map_info` list mutations (map, unmap, protect) + - read/write don't need locks if backed by shared mmap (atomic at OS level) +- [ ] `QlFileDes`: lock FD table mutations (open, close, dup) +- [ ] Hook lists: lock registration/deregistration (hooks are usually set up before + `run()`, so contention should be minimal) +- [ ] Logging: thread-safe log handler with thread ID prefix + +### 3.5 Validation + +- [ ] pthread_create / pthread_join +- [ ] Mutex: two threads incrementing a shared counter with proper locking +- [ ] Condition variables: producer-consumer +- [ ] Futex: custom futex-based synchronization +- [ ] Thread-local storage (TLS) correctness per architecture +- [ ] Stress test: 10+ threads doing concurrent work +- [ ] ALL existing single-threaded tests still pass +- [ ] ALL existing gevent-threaded tests still pass (gevent mode preserved as fallback) + +**Risk**: HIGH — changes to memory manager, Unicorn integration, and thread model. +Keep the existing gevent threading as a fallback mode. The new threading is opt-in. + +--- + +## Phase 4: Signals + +**Depends on Phase 3** (real threads required for proper signal delivery). + +### 4.1 Signal handler registration + +- [ ] Forward `sigaction(signum, act, oldact)` to kernel proxy +- [ ] Forward `sigprocmask` / `rt_sigprocmask` +- [ ] Forward `sigaltstack` + +### 4.2 Signal delivery + +When a signal is delivered to a proxy thread: + +- [ ] Proxy catches the signal and sends notification to Qiling via IPC +- [ ] Qiling calls `emu_stop()` on the target thread's Unicorn +- [ ] Save thread context (registers) +- [ ] Build signal frame on emulated stack (architecture-specific) +- [ ] Set PC to the registered signal handler +- [ ] Resume Unicorn — handler executes in emulated code +- [ ] On `sigreturn` / `rt_sigreturn`: restore saved context, resume normal execution + +### 4.3 Signal-syscall interaction + +- [ ] `EINTR` on interrupted blocking syscalls +- [ ] `SA_RESTART` flag: automatically restart interrupted syscalls +- [ ] `kill()`, `tgkill()`, `tkill()` → forward to kernel + +### 4.4 Validation + +- [ ] SIGALRM handler (timer-based) +- [ ] SIGCHLD on child exit +- [ ] SIGPIPE on broken pipe +- [ ] Signal interrupting `read()` — verify EINTR +- [ ] Custom signal handler that modifies emulated state + +**Risk**: MEDIUM — signal frame construction is architecture-specific and fiddly, +but the mechanism is well-understood. Main risk is getting the frame layout exactly +right for each architecture. + +--- + +## Phase 5: Integration and Polish + +### 5.1 User-facing API + +```python +# Opt-in to hybrid kernel +ql = Qiling(argv=[...], rootfs="...") + +# Enable kernel proxy for networking (Phase 1-2) +ql.os.kernel_proxy.enable(networking=True) + +# Enable real threading (Phase 3) — requires networking=True +ql.os.kernel_proxy.enable(networking=True, threading=True) + +# Enable signals (Phase 4) — requires threading=True +ql.os.kernel_proxy.enable(networking=True, threading=True, signals=True) + +# Configure network namespace +ql.os.kernel_proxy.network_mode = "bridged" # "host" | "isolated" | "bridged" + +# User hooks still work — they fire before/after proxy forwarding +ql.os.set_syscall("connect", my_connect_hook, QL_INTERCEPT.ENTER) +``` + +### 5.2 Backward compatibility + +- [ ] Default behavior: no proxy, existing Python handlers — zero regression +- [ ] All existing tests pass with proxy disabled +- [ ] All existing tests pass with proxy enabled (forwarded syscalls should + produce equivalent results) +- [ ] `set_syscall()` user hooks fire correctly in both modes +- [ ] Existing gevent threading preserved as fallback when real threading not enabled + +### 5.3 Fallback on failure + +- [ ] If proxy process crashes: log error, fall back to Python handlers, warn user +- [ ] If proxy not available (non-Linux host): use Python handlers, warn user +- [ ] Graceful degradation: never crash, always fall back + +### 5.4 Platform support + +- [ ] Linux host: full support (namespaces, real threading) +- [ ] macOS host: proxy via Docker/Lima (networking only, no native namespaces) +- [ ] Windows host: proxy via WSL2 (networking only) +- [ ] Document host requirements + +### 5.5 Performance + +- [ ] Benchmark: syscall latency (Python handler vs proxy round-trip) +- [ ] Optimize IPC: shared memory ring buffer for high-frequency syscalls +- [ ] Batch small syscalls where possible +- [ ] Profile and tune for common workloads (network servers, threaded computation) + +--- + +## Existing Issues (Independent of Hybrid Architecture) + +These should be fixed regardless of the hybrid work. + +### Bare except blocks swallowing errors + +10+ bare `except:` blocks silently hide failures: + +- `qiling/utils.py:242` — PE detection +- `qiling/debugger/qdb/qdb.py:128,352,598` — debugger operations +- `qiling/os/posix/filestruct.py:62,173,179` — fcntl/ioctl +- `qiling/os/posix/syscall/select.py:78` — select failures +- `qiling/os/windows/registry.py:127,185` — registry operations + +### Asserts used for validation + +Assertions disabled with `python -O`. Replace with exceptions: + +- `qiling/os/memory.py` — page alignment, size, mapping checks +- `qiling/arch/x86_utils.py` — GDT/segment validation +- `qiling/cc/__init__.py` — calling convention validation + +### Memory manager: string label parsing + +`qiling/os/memory.py:209-218` — `get_lib_base()` uses regex on info strings. +Needs a proper mapping structure. + +### ARM Thumb mode detection + +`core.py:753` — fragile `_init_thumb` flag. Needs upstream Unicorn fix. + +### x86 GDT privilege levels + +`qiling/arch/x86_utils.py:147,178` — ring 3 forced to ring 0. + +### Unbounded `read_cstring` + +`qiling/os/memory.py:51-63` — no length limit. Can hang on MMIO. + +### Incomplete save/restore + +`QlOs.save()/restore()` empty in base class. UEFI and Windows don't implement it. + +### Incomplete Windows emulation + +Fiber, registry, handle management, DLL resolution gaps. +See `qiling/os/windows/` TODO comments. + +### macOS and UEFI gaps + +- macOS kext: 5 FIXMEs in `macos.py:79-117` +- UEFI variables: `uefi/rt.py:204-205` + +### Hook system cleanup + +- `type()` vs `isinstance()` in `core_hooks.py` +- Unclear return value semantics +- Non-intuitive `begin=1, end=0` for "entire memory" + +### Hardcoded magic numbers + +- Exit points in `os/os.py:84-87` +- Guard page `0x9000000` in `core.py:525` + +### Test coverage + +- ARM test skipped (`test_elf.py:411`) +- Multithread test skipped (`test_elf_multithread.py:185`) +- Broken wchar (`test_struct.py:170,185`) +- PowerPC, QNX, DOS, MCU: minimal coverage diff --git a/examples/rootfs b/examples/rootfs index 120fb6d37..6d4d654fd 160000 --- a/examples/rootfs +++ b/examples/rootfs @@ -1 +1 @@ -Subproject commit 120fb6d37700a2d4c0e35ced599aaee7a8f98723 +Subproject commit 6d4d654fdc2892490d98c433eca3efa5c6d062c7 diff --git a/poetry.lock b/poetry.lock index 3933b97cf..189014727 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,24 +1,58 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. + +[[package]] +name = "antlr4-python3-runtime" +version = "4.8" +description = "ANTLR 4.8 runtime for Python 3.7" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "antlr4-python3-runtime-4.8.tar.gz", hash = "sha256:15793f5d0512a372b4e7d2284058ad32ce7dd27126b105fb0b2245130445db33"}, +] [[package]] name = "antlr4-python3-runtime" version = "4.13.2" description = "ANTLR 4.13.2 runtime for Python 3" -category = "main" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8"}, {file = "antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916"}, ] +[[package]] +name = "asciimatics" +version = "1.14.0" +description = "A cross-platform package to replace curses (mouse/keyboard input & text colours/positioning) and create ASCII animations" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "asciimatics-1.14.0-py2.py3-none-any.whl", hash = "sha256:277fe925d0d7a029b35245cde01ead009b4a1336130543ace5c8821f38df1da7"}, + {file = "asciimatics-1.14.0.tar.gz", hash = "sha256:16d20ce42210b434eb05ba469ecdb8293ac7ed3c0ce0dd4f70e30d72d7602227"}, +] + +[package.dependencies] +future = "*" +Pillow = ">=2.7.0" +pyfiglet = ">=0.7.2" +pywin32 = {version = "*", markers = "sys_platform == \"win32\""} +wcwidth = "*" + [[package]] name = "asciimatics" version = "1.15.0" description = "A cross-platform package to replace curses (mouse/keyboard input & text colours/positioning) and create ASCII animations" -category = "main" optional = false python-versions = ">= 3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "asciimatics-1.15.0-py3-none-any.whl", hash = "sha256:0fe068a6bed522929bd04bb5b8a2fb6ebf0aef1b7a9b3843cf71030a34bc38d5"}, {file = "asciimatics-1.15.0.tar.gz", hash = "sha256:cfdd398042727519d8b73e62b8ef82c0becfed4eb420899c3b96c98d0b96821a"}, @@ -32,26 +66,36 @@ wcwidth = "*" [[package]] name = "capstone" -version = "4.0.2" +version = "5.0.7" description = "Capstone disassembly engine" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "capstone-4.0.2-py2.py3-none-manylinux1_i686.whl", hash = "sha256:da442f979414cf27e4621e70e835880878c858ea438c4f0e957e132593579e37"}, - {file = "capstone-4.0.2-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:9d1a9096c5f875b11290317722ed44bb6e7c52e50cc79d791f142bce968c49aa"}, - {file = "capstone-4.0.2-py2.py3-none-win32.whl", hash = "sha256:c3d9b443d1adb40ee2d9a4e7341169b76476ddcf3a54c03793b16cdc7cd35c5a"}, - {file = "capstone-4.0.2-py2.py3-none-win_amd64.whl", hash = "sha256:0d65ffe8620920976ceadedc769f22318f6f150a592368d8a735612367ac8a1a"}, - {file = "capstone-4.0.2.tar.gz", hash = "sha256:2842913092c9b69fd903744bc1b87488e1451625460baac173056e1808ec1c66"}, + {file = "capstone-5.0.7-py3-none-macosx_10_9_universal2.whl", hash = "sha256:388af4ddb9224d3b4f9269673ee575b3f94f77774d48b3f1a283ad13c29a106a"}, + {file = "capstone-5.0.7-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:a9f64e3d75d8c4d7b3d26bba153b2992aadcf6b8d57674b4ef176b4ecdd9822f"}, + {file = "capstone-5.0.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:acb89f5bf6f625745a104a3a44819d3acea173228055c1eadc60d2282ae490bb"}, + {file = "capstone-5.0.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c58546c814567c95e4b9a63bdb8624c960cb8508855c7c767d5f108d7bc09ce2"}, + {file = "capstone-5.0.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b809a9654844ce0d35099121a851ddd2ab2689df1ff6687037babcedcaae6391"}, + {file = "capstone-5.0.7-py3-none-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b0f1b93fc703c419fda8cf84cfa017fd8909be62a4e88024273126ab16f006"}, + {file = "capstone-5.0.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:467716e6555d50cb3526b290f0dbdccb5f961839b1f1e299b484fb5d814173e6"}, + {file = "capstone-5.0.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e551311d4b6dc344fe5518ef6decf4c2dfafe37bba9ad027a53a406930bc5c63"}, + {file = "capstone-5.0.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a13437b28b136c886600e88bee192d25adf56ba1db5597ff5a0bec758bb9c533"}, + {file = "capstone-5.0.7-py3-none-win_amd64.whl", hash = "sha256:4ab8bcb7da8f221ff45926ca168ca33e76f7237d06fbf3c10780002faa2670e1"}, + {file = "capstone-5.0.7.tar.gz", hash = "sha256:796bdd69b05fa124fc2aa2e74b9a0b3d4c4e7f3e02add5e583cf2f3bca282ede"}, ] +[package.dependencies] +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} + [[package]] name = "cffi" version = "1.17.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -129,9 +173,9 @@ pycparser = "*" name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -144,9 +188,10 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -156,9 +201,9 @@ files = [ name = "dacite" version = "1.8.1" description = "Simple creation of data classes from dictionaries." -category = "main" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "dacite-1.8.1-py3-none-any.whl", hash = "sha256:cc31ad6fdea1f49962ea42db9421772afe01ac5442380d9a99fcf3d188c61afe"}, ] @@ -170,9 +215,9 @@ dev = ["black", "coveralls", "mypy", "pre-commit", "pylint", "pytest (>=5)", "py name = "dill" version = "0.3.9" description = "serialize all of Python" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, @@ -186,9 +231,9 @@ profile = ["gprof2dot (>=2022.7.29)"] name = "enum-compat" version = "0.0.3" description = "enum/enum34 compatibility package" -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "enum-compat-0.0.3.tar.gz", hash = "sha256:3677daabed56a6f724451d585662253d8fb4e5569845aafa8bb0da36b1a8751e"}, {file = "enum_compat-0.0.3-py3-none-any.whl", hash = "sha256:88091b617c7fc3bbbceae50db5958023c48dc40b50520005aa3bf27f8f7ea157"}, @@ -198,21 +243,34 @@ files = [ name = "first" version = "2.0.2" description = "Return the first true value of an iterable." -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "first-2.0.2-py2.py3-none-any.whl", hash = "sha256:8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86"}, {file = "first-2.0.2.tar.gz", hash = "sha256:ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"}, ] +[[package]] +name = "future" +version = "0.18.3" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, +] + [[package]] name = "fuzzercorn" version = "0.0.1" description = "Libfuzzer bindings for Unicorn.." -category = "main" optional = true python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Linux\" and extra == \"fuzz\"" files = [ {file = "fuzzercorn-0.0.1-py3-none-manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:304ddcd19803c779a8e4a6a900c51f7c3f5ba0e8c3322bd9502d59ea0825d424"}, {file = "fuzzercorn-0.0.1-py3-none-manylinux1_i686.whl", hash = "sha256:7fa9cbffbcbf45c0af5707abc86ee8fa0d3a396f0ff48ce67c355ef9d3047c4f"}, @@ -228,9 +286,9 @@ unicorn = ">=2.0.0rc5" name = "gevent" version = "24.2.1" description = "Coroutine-based network library" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "gevent-24.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f947a9abc1a129858391b3d9334c45041c08a0f23d14333d5b844b6e5c17a07"}, {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde283313daf0b34a8d1bab30325f5cb0f4e11b5869dbe5bc61f8fe09a8f66f3"}, @@ -285,19 +343,20 @@ greenlet = [ "zope.interface" = "*" [package.extras] -dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] +dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""] docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] -monitor = ["psutil (>=5.7.0)"] -recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] -test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests"] +monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +recommended = ["cffi (>=1.12.2) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +test = ["cffi (>=1.12.2) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"] [[package]] name = "greenlet" version = "3.1.1" description = "Lightweight in-process concurrent programming" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\"" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -382,9 +441,10 @@ test = ["objgraph", "psutil"] name = "importlib-resources" version = "6.4.5" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.8\"" files = [ {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, @@ -394,20 +454,37 @@ files = [ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] type = ["pytest-mypy"] +[[package]] +name = "jsonpath-ng" +version = "1.6.0" +description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "jsonpath-ng-1.6.0.tar.gz", hash = "sha256:5483f8e9d74c39c9abfab554c070ae783c1c8cbadf5df60d561bc705ac68a07e"}, + {file = "jsonpath_ng-1.6.0-py3-none-any.whl", hash = "sha256:6fd04833412c4b3d9299edf369542f5e67095ca84efa17cbb7f06a34958adc9f"}, +] + +[package.dependencies] +ply = "*" + [[package]] name = "jsonpath-ng" version = "1.6.1" description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." -category = "main" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "jsonpath-ng-1.6.1.tar.gz", hash = "sha256:086c37ba4917304850bd837aeab806670224d3f038fe2833ff593a672ef0a5fa"}, {file = "jsonpath_ng-1.6.1-py3-none-any.whl", hash = "sha256:8f22cd8273d7772eea9aaa84d922e0841aa36fdb8a2c6b7f6c3791a16a9bc0be"}, @@ -420,9 +497,9 @@ ply = "*" name = "keystone-engine" version = "0.9.2" description = "Keystone assembler engine" -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "keystone-engine-0.9.2.tar.gz", hash = "sha256:2f7af62dab0ce6c2732dbb4f31cfa2184a8a149e280b96b92ebc0db84c6e50f5"}, {file = "keystone_engine-0.9.2-py2.py3-none-macosx_10_14_x86_64.whl", hash = "sha256:dafcc3d9450c239cbc54148855b79c4b387777099c6d054005c835768cf955f2"}, @@ -436,9 +513,9 @@ files = [ name = "loguru" version = "0.7.2" description = "Python logging made (stupidly) simple" -category = "main" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, @@ -449,15 +526,15 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] +dev = ["Sphinx (==7.2.5) ; python_version >= \"3.9\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.2.2) ; python_version >= \"3.8\"", "mypy (==0.910) ; python_version < \"3.6\"", "mypy (==0.971) ; python_version == \"3.6\"", "mypy (==1.4.1) ; python_version == \"3.7\"", "mypy (==1.5.1) ; python_version >= \"3.8\"", "pre-commit (==3.4.0) ; python_version >= \"3.8\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==7.4.0) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==4.1.0) ; python_version >= \"3.8\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.0.0) ; python_version >= \"3.8\"", "sphinx-autobuild (==2021.3.14) ; python_version >= \"3.9\"", "sphinx-rtd-theme (==1.3.0) ; python_version >= \"3.9\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.11.0) ; python_version >= \"3.8\""] [[package]] name = "multiprocess" version = "0.70.17" description = "better multiprocessing and multithreading in Python" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "multiprocess-0.70.17-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ddb24e5bcdb64e90ec5543a1f05a39463068b6d3b804aa3f2a4e16ec28562d6"}, {file = "multiprocess-0.70.17-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d729f55198a3579f6879766a6d9b72b42d4b320c0dcb7844afb774d75b573c62"}, @@ -480,13 +557,27 @@ files = [ [package.dependencies] dill = ">=0.3.9" +[[package]] +name = "overrides" +version = "7.4.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"}, + {file = "overrides-7.4.0.tar.gz", hash = "sha256:9502a3cca51f4fac40b5feca985b6703a5c1f6ad815588a7ca9e285b9dca6757"}, +] + [[package]] name = "overrides" version = "7.7.0" description = "A decorator to automatically detect mismatch when overriding a method." -category = "main" optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, @@ -496,21 +587,91 @@ files = [ name = "pefile" version = "2024.8.26" description = "Python PE parsing module" -category = "main" optional = false python-versions = ">=3.6.0" +groups = ["main"] files = [ {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, ] +[[package]] +name = "pillow" +version = "10.0.1" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "pillow" version = "10.4.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, @@ -599,16 +760,16 @@ docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] name = "ply" version = "3.11" description = "Python Lex & Yacc" -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, @@ -618,9 +779,9 @@ files = [ name = "prompt-toolkit" version = "3.0.50" description = "Library for building powerful interactive command lines in Python" -category = "main" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, @@ -633,9 +794,10 @@ wcwidth = "*" name = "pycparser" version = "2.22" description = "C parser in Python" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -645,44 +807,104 @@ files = [ name = "pyelftools" version = "0.32" description = "Library for analyzing ELF files and DWARF debugging information" -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyelftools-0.32-py3-none-any.whl", hash = "sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738"}, {file = "pyelftools-0.32.tar.gz", hash = "sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5"}, ] +[[package]] +name = "pyfiglet" +version = "0.8.post1" +description = "Pure-python FIGlet implementation" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "pyfiglet-0.8.post1-py2.py3-none-any.whl", hash = "sha256:d555bcea17fbeaf70eaefa48bb119352487e629c9b56f30f383e2c62dd67a01c"}, + {file = "pyfiglet-0.8.post1.tar.gz", hash = "sha256:c6c2321755d09267b438ec7b936825a4910fec696292139e664ca8670e103639"}, +] + [[package]] name = "pyfiglet" version = "1.0.2" description = "Pure-python FIGlet implementation" -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "pyfiglet-1.0.2-py3-none-any.whl", hash = "sha256:889b351d79c99e50a3f619c8f8e6ffdb27fd8c939fc43ecbd7559bd57d5f93ea"}, {file = "pyfiglet-1.0.2.tar.gz", hash = "sha256:758788018ab8faaddc0984e1ea05ff330d3c64be663c513cc1f105f6a3066dab"}, ] +[[package]] +name = "pyperclip" +version = "1.8.2" +description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, +] + [[package]] name = "pyperclip" version = "1.9.0" description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" -category = "main" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, ] +[[package]] +name = "python-fx" +version = "0.3.1" +description = "A python-native fx-alike terminal JSON viewer." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "python-fx-0.3.1.tar.gz", hash = "sha256:76044ba32195b8e0ce444aa714981cb1481f9df44c3381c5ed4b43a4f6812c73"}, + {file = "python_fx-0.3.1-py3-none-any.whl", hash = "sha256:e7cfbb8421831aaff5684dd1ae6ec855b92bcd089f9f76b06a3f2baa4670447a"}, +] + +[package.dependencies] +antlr4-python3-runtime = "4.8" +asciimatics = "1.14.0" +click = {version = "8.1.7", markers = "python_version >= \"3.7\""} +dacite = {version = "1.8.1", markers = "python_version >= \"3.6\""} +first = "2.0.2" +future = {version = "0.18.3", markers = "python_version >= \"2.6\" and python_version not in \"3.0, 3.1, 3.2, 3.3\""} +jsonpath-ng = "1.6.0" +loguru = {version = "0.7.2", markers = "python_version >= \"3.5\""} +overrides = {version = "7.4.0", markers = "python_version >= \"3.6\""} +pillow = {version = "10.0.1", markers = "python_version >= \"3.8\""} +ply = "3.11" +pyfiglet = {version = "0.8.post1", markers = "python_version >= \"3.9\""} +pyperclip = "1.8.2" +pyyaml = {version = "6.0.1", markers = "python_version >= \"3.6\""} +urwid = {version = "2.2.1", markers = "python_full_version >= \"3.7.0\""} +wcwidth = "0.2.6" +yamale = {version = "4.0.4", markers = "python_version >= \"3.6\""} + [[package]] name = "python-fx" version = "0.3.2" description = "A python-native fx-alike terminal JSON viewer." -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "python_fx-0.3.2-py3-none-any.whl", hash = "sha256:5498475b0f391b1649732328b58d188d9fc4b3f90f5bfb77d5c6e2ece2432c5f"}, {file = "python_fx-0.3.2.tar.gz", hash = "sha256:9646f58c716e2db6698bff3dfa55fa721b8b0cb741506287a87bc08055a96ceb"}, @@ -711,9 +933,9 @@ yamale = {version = "5.2.1", markers = "python_version >= \"3.8\""} name = "python-registry" version = "1.3.1" description = "Read access to Windows Registry files." -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "python-registry-1.3.1.tar.gz", hash = "sha256:99185f67d5601be3e7843e55902d5769aea1740869b0882f34ff1bd4b43b1eb2"}, {file = "python_registry-1.3.1-py2-none-any.whl", hash = "sha256:59d3b00c04bca0c4e1a12be0404da6ccf76b87537ee3a3ad2d8fc1bccf6f63ca"}, @@ -728,9 +950,10 @@ unicodecsv = "*" name = "pywin32" version = "308" description = "Python for Window Extensions" -category = "main" optional = false python-versions = "*" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, @@ -752,13 +975,76 @@ files = [ {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -819,9 +1105,9 @@ files = [ name = "questionary" version = "2.1.0" description = "Python library to build pretty command line user prompts ⭐️" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec"}, {file = "questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587"}, @@ -834,9 +1120,10 @@ prompt_toolkit = ">=2.0,<4.0" name = "r2libr" version = "5.7.4" description = "Yet anohter radare2 python bindings." -category = "main" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"re\"" files = [ {file = "r2libr-5.7.4-py3-none-macosx_10_15_universal2.whl", hash = "sha256:c752e57085fed9d34527d2ff6692068c2a162c3477c0539e9e61cd7eb5acdcfc"}, {file = "r2libr-5.7.4-py3-none-manylinux1_x86_64.whl", hash = "sha256:80df7902492de77e2cb770ecb7ffbe215908192962d95f0e43b42d3c3d464096"}, @@ -847,30 +1134,30 @@ files = [ name = "setuptools" version = "75.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.5.2) ; sys_platform != \"cygwin\""] +core = ["importlib-metadata (>=6) ; python_version < \"3.10\"", "importlib-resources (>=5.10.2) ; python_version < \"3.9\"", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12.0,<1.13.0)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.12.*)", "pytest-mypy"] [[package]] name = "termcolor" version = "2.4.0" description = "ANSI color formatting for output in terminal" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, @@ -883,9 +1170,10 @@ tests = ["pytest", "pytest-cov"] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -895,9 +1183,9 @@ files = [ name = "unicodecsv" version = "0.14.1" description = "Python2's stdlib csv module is nice, but it doesn't support unicode. This module is a drop-in replacement which *does*." -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "unicodecsv-0.14.1.tar.gz", hash = "sha256:018c08037d48649a0412063ff4eda26eaa81eff1546dbffa51fa5293276ff7fc"}, ] @@ -906,9 +1194,9 @@ files = [ name = "unicorn" version = "2.1.3" description = "Unicorn CPU emulator engine" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] files = [ {file = "unicorn-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cbf99c139a238ee6ccfaadea35e65a88461c0ae0dcf78058c8266ff90f8866c"}, {file = "unicorn-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e7b9396a7b76503b1d32c4b83d35e03e8b2ee81e80a2c7aee77dac7b71f25c"}, @@ -989,15 +1277,16 @@ files = [ importlib_resources = {version = "*", markers = "python_version < \"3.9\""} [package.extras] -test = ["capstone (==5.0.1)", "capstone (==6.0.0a2)"] +test = ["capstone (==5.0.1) ; python_version <= \"3.7\"", "capstone (==6.0.0a2) ; python_version > \"3.7\""] [[package]] name = "unicornafl" version = "2.1.0" description = "Unicornafl" -category = "main" optional = true python-versions = "*" +groups = ["main"] +markers = "platform_system != \"Windows\" and extra == \"fuzz\"" files = [ {file = "unicornafl-2.1.0-py3-none-macosx_11_7_x86_64.whl", hash = "sha256:8827c010376274730776b85f884cf9037d08ca6cd866b364bcfea8eee6db6090"}, {file = "unicornafl-2.1.0-py3-none-manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa3130b3c811291719874cfd7e728ce0c8147cc63c8853069a5195e528bafb43"}, @@ -1010,13 +1299,60 @@ files = [ [package.dependencies] unicorn = ">=2.0.1" +[[package]] +name = "urwid" +version = "2.2.1" +description = "A full-featured console (xterm et al.) user interface library" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "urwid-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7aa95e2f8941e323f0534a301f9d8d965d869110d326b3c9dff63e1c116772cd"}, + {file = "urwid-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ea22e5eabace2c66e2f52cc2494308c1a0091bcb89b3ceedf72ff91733f4dbb2"}, + {file = "urwid-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07b88bdc8efe95b318201bd054ae69bed68bb9f506f127b21ae7234ffb7db3a4"}, + {file = "urwid-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba58f24fdff58975ef603ee803909d57faaaebd407fd50042652dcc9a8dd2f2"}, + {file = "urwid-2.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab2d5704cbd32f729a60d2b56d076e16652b3b97ebe6773c54a192cb9f49c169"}, + {file = "urwid-2.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc88edae9cb34644905e3d00e9fd2dc9a2c1eaeb2e311c1aec0d36a51d77b10"}, + {file = "urwid-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2fa3e83730a811466d272ab68340f9f9418ee7ca5f6de3548dd7a5661eafbbee"}, + {file = "urwid-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f87d7efbbf1d716bbcf025d453b3481aa0d9e1c91581aa8edc9ae7af64efa85"}, + {file = "urwid-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdafd72d6a539e16e6c179dd16609601643b85edf97b1543fc208e4fb7e6c249"}, + {file = "urwid-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e17eed4190220873531e2c11c885764d2e3bcabe9e35d5a578e84056a2c58199"}, + {file = "urwid-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7949a8d4384d170e4dfa41151e6264f6238b3ba2520649c25110bc6451978568"}, + {file = "urwid-2.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c96c57714fd0eac79a5e8a9d38d15d68ab9b6a96c2fe282ccd61cb707dd4be2"}, + {file = "urwid-2.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1bee2f966063de86a093908abb5bd56910e5d5630e021b351f240ab3b972207e"}, + {file = "urwid-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:57e28adf4457fb50b751838836bb94122e904ccad4429d42c4f318a3287a4802"}, + {file = "urwid-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad5ff26140b66ebb69957c51fd168a86f212adb578c83ed590ffd7e032da973f"}, + {file = "urwid-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:653b1fb9c52c4a32c326f701dd3ceb6edd1f30f32033c040fd5edc55d3d60cdb"}, + {file = "urwid-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4438be6b4b303d1012b8208cf5ff2ef71bdc19a7732771257ece36e2d1d16283"}, + {file = "urwid-2.2.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868c2cfd3fadd6cf42a4a2dbb2cf87a92d6c12dc5ed8b991ff96e66f0ba8c38"}, + {file = "urwid-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c3f00ca72de0736f3df4971a01d2278065628bef179fbd4fce37aacf93bbeb2c"}, + {file = "urwid-2.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5354319e3ac4e612a4a280421e21fa4243023df73e48ee701e4e944e769d87c"}, + {file = "urwid-2.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f3fdbd58a3ef1f19393a5f1d8b61adcfc89d0e235a2e05927cbecbf8012120c"}, + {file = "urwid-2.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55d13d1c6f15bd594a49ad165837edf34678c7c2362834f0d771990821e3bb8c"}, + {file = "urwid-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93f081f1c53d7d307694ae20eb07ac731b4337d517dd6ee9dd91bae78bcb67bf"}, + {file = "urwid-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d42a9e4939d18c77f73183593a589c9b3b5d5fa3615d94a32e15cd97b00d3536"}, + {file = "urwid-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5eeb4e1760e9356471f8b50e3296c24881e242aae57f738a6f8534438848fb2e"}, + {file = "urwid-2.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d894543b4f3f3f2ce9837782e45cc3797df0a1697264a2939a2391076d07f641"}, + {file = "urwid-2.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3205f486e9fa4c6193aa5b9623abe05db864465acd02825305702849572c0828"}, + {file = "urwid-2.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccfbbc37565f24156e2bdb37504b010fedee8f4a70cfc353c0d9782354087484"}, + {file = "urwid-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4add23f02ef43497c13592e1804640b8b19fe781e8d62f445000c7acca60e2e2"}, + {file = "urwid-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:476f03705095fed744413d8256c7da998694b2f81e7e6e665e6244d1d3159d1e"}, + {file = "urwid-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85614e0436eb8c77bb21950f7d52bf93668b4ba8a11a2986bc111b48d28390f6"}, + {file = "urwid-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf09c272b46ec0f78b6eaf2a515ded30e952a0e77dbbb3535593d3f05354eb82"}, + {file = "urwid-2.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8de3025ee488d9f56db9e8842d6c7f1c290e01fd6749320d8c171fef5cbef35"}, + {file = "urwid-2.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2eea5fba3dab9f0977fcf17608370686da9d18a6077fd180a98eba72c59ff5d"}, + {file = "urwid-2.2.1.tar.gz", hash = "sha256:e33911ab18f2c73fddbe9bf216d021e74e20b2d5aa9be30403c58f55131bb8a1"}, +] + [[package]] name = "urwid" version = "2.6.15" description = "A full-featured console (xterm et al.) user interface library" -category = "main" optional = false python-versions = ">3.7" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "urwid-2.6.15-py3-none-any.whl", hash = "sha256:71b3171cabaa0092902f556768756bd2f2ebb24c0da287ee08f081d235340cb7"}, {file = "urwid-2.6.15.tar.gz", hash = "sha256:9ecc57330d88c8d9663ffd7092a681674c03ff794b6330ccfef479af7aa9671b"}, @@ -1027,7 +1363,7 @@ typing-extensions = "*" wcwidth = "*" [package.extras] -curses = ["windows-curses"] +curses = ["windows-curses ; sys_platform == \"win32\""] glib = ["PyGObject"] lcd = ["pyserial"] serial = ["pyserial"] @@ -1036,13 +1372,27 @@ trio = ["exceptiongroup", "trio (>=0.22.0)"] twisted = ["twisted"] zmq = ["zmq"] +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + [[package]] name = "wcwidth" version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" -category = "main" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -1052,24 +1402,26 @@ files = [ name = "win32-setctime" version = "1.2.0" description = "A small Python utility to set file creation time on Windows" -category = "main" optional = false python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, ] [package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [[package]] name = "windows-curses" version = "2.4.1" description = "Support for the standard curses module on Windows" -category = "main" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ {file = "windows_curses-2.4.1-cp310-cp310-win32.whl", hash = "sha256:53d711e07194d0d3ff7ceff29e0955b35479bc01465d46c3041de67b8141db2f"}, {file = "windows_curses-2.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:325439cd4f37897a1de8a9c068a5b4c432f9244bf9c855ee2fbeb3fa721a770c"}, @@ -1089,13 +1441,30 @@ files = [ {file = "windows_curses-2.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:4588213f7ef3b0c24c5cb9e309653d7a84c1792c707561e8b471d466ca79f2b8"}, ] +[[package]] +name = "yamale" +version = "4.0.4" +description = "A schema and validator for YAML." +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "python_version < \"3.11\" or platform_python_implementation != \"CPython\"" +files = [ + {file = "yamale-4.0.4-py3-none-any.whl", hash = "sha256:04f914c0886bda03ac20f8468272cfd9374a634a062549490eff2beedeb30497"}, + {file = "yamale-4.0.4.tar.gz", hash = "sha256:e524caf71cbbbd15aa295e8bdda01688ac4b5edaf38dd60851ddff6baef383ba"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "yamale" version = "5.2.1" description = "A schema and validator for YAML." -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\"" files = [ {file = "yamale-5.2.1-py3-none-any.whl", hash = "sha256:e44cd30cf3055ee4b34c1c71d6fe35490a127dcbd36f82f27859d105a9989922"}, {file = "yamale-5.2.1.tar.gz", hash = "sha256:19bbe713d588f07177bc519a46070c0793ed126ea37f425a76055b99703f835a"}, @@ -1108,29 +1477,30 @@ pyyaml = "*" name = "zipp" version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.8\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [[package]] name = "zope-event" version = "5.0" description = "Very basic event publishing system" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, @@ -1147,9 +1517,9 @@ test = ["zope.testrunner"] name = "zope-interface" version = "7.2" description = "Interfaces for Python" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"}, {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"}, @@ -1199,10 +1569,10 @@ test = ["coverage[toml]", "zope.event", "zope.testing"] testing = ["coverage[toml]", "zope.event", "zope.testing"] [extras] -fuzz = ["unicornafl", "fuzzercorn"] +fuzz = ["fuzzercorn", "unicornafl"] re = ["r2libr"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.8" -content-hash = "f0a6ca5220bfd011fc4c9e54e151f7752bcbd5b4cf3d80698cfc4f3a53665aa4" +content-hash = "9191e91c28fe22a05ac8185ae5fd4a1fc969cecad63b41d649981dfbc50e86ae" diff --git a/qiling/exception.py b/qiling/exception.py index e6d460609..a544b254f 100644 --- a/qiling/exception.py +++ b/qiling/exception.py @@ -74,6 +74,9 @@ class QlMemoryMappedError(QlErrorBase): class QlGDTError(QlErrorBase): pass +class QlProxyConnectionError(QlErrorBase): + pass + class QlSyscallError(QlErrorBase): def __init__(self, errno, msg): super(QlSyscallError, self).__init__(msg) diff --git a/qiling/os/linux/map_syscall.py b/qiling/os/linux/map_syscall.py index 0584d58d2..c3a1289e7 100644 --- a/qiling/os/linux/map_syscall.py +++ b/qiling/os/linux/map_syscall.py @@ -2223,6 +2223,7 @@ def __mapper(syscall_num: int) -> str: 432: "fsmount", 433: "fspick", 434: "pidfd_open", + 435: "clone3", 436: "close_range", 437: "openat2", 438: "pidfd_getfd", @@ -2234,7 +2235,22 @@ def __mapper(syscall_num: int) -> str: 444: "landlock_create_ruleset", 445: "landlock_add_rule", 446: "landlock_restrict_self", + 447: "memfd_secret", 448: "process_mrelease", + 449: "futex_waitv", + 450: "set_mempolicy_home_node", + 451: "cachestat", + 452: "fchmodat2", + 453: "map_shadow_stack", + 454: "futex_wake", + 455: "futex_wait", + 456: "futex_requeue", + 457: "statmount", + 458: "listmount", + 459: "lsm_get_self_attr", + 460: "lsm_set_self_attr", + 461: "lsm_list_modules", + 462: "mseal", 1024: "open", 1025: "link", 1026: "unlink", @@ -2549,7 +2565,22 @@ def __mapper(syscall_num: int) -> str: 444: "landlock_create_ruleset", 445: "landlock_add_rule", 446: "landlock_restrict_self", + 447: "memfd_secret", 448: "process_mrelease", + 449: "futex_waitv", + 450: "set_mempolicy_home_node", + 451: "cachestat", + 452: "fchmodat2", + 453: "map_shadow_stack", + 454: "futex_wake", + 455: "futex_wait", + 456: "futex_requeue", + 457: "statmount", + 458: "listmount", + 459: "lsm_get_self_attr", + 460: "lsm_set_self_attr", + 461: "lsm_list_modules", + 462: "mseal", } ppc_syscall_table = { @@ -2968,4 +2999,33 @@ def __mapper(syscall_num: int) -> str: 431: "fsconfig", 432: "fsmount", 433: "fspick", + 434: "pidfd_open", + 435: "clone3", + 436: "close_range", + 437: "openat2", + 438: "pidfd_getfd", + 439: "faccessat2", + 440: "process_madvise", + 441: "epoll_pwait2", + 442: "mount_setattr", + 443: "quotactl_fd", + 444: "landlock_create_ruleset", + 445: "landlock_add_rule", + 446: "landlock_restrict_self", + 447: "memfd_secret", + 448: "process_mrelease", + 449: "futex_waitv", + 450: "set_mempolicy_home_node", + 451: "cachestat", + 452: "fchmodat2", + 453: "map_shadow_stack", + 454: "futex_wake", + 455: "futex_wait", + 456: "futex_requeue", + 457: "statmount", + 458: "listmount", + 459: "lsm_get_self_attr", + 460: "lsm_set_self_attr", + 461: "lsm_list_modules", + 462: "mseal", } diff --git a/qiling/os/posix/kernel_proxy/__init__.py b/qiling/os/posix/kernel_proxy/__init__.py new file mode 100644 index 000000000..f7ac75eab --- /dev/null +++ b/qiling/os/posix/kernel_proxy/__init__.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +""" +Hybrid kernel proxy — forward specific syscalls to a real Linux kernel. + +Usage: + from qiling import Qiling + from qiling.os.posix.kernel_proxy import KernelProxy + + ql = Qiling(argv=["/bin/myserver"], rootfs="rootfs/x8664_linux") + proxy = KernelProxy(ql) + proxy.forward_syscall("epoll_create", returns_fd=True) + proxy.forward_syscall("epoll_ctl") + proxy.forward_syscall("epoll_wait") + ql.run() +""" + +from __future__ import annotations + +import os +import sys +import socket +import subprocess +from typing import Dict, Optional, TYPE_CHECKING + +from qiling.const import QL_INTERCEPT, QL_OS +from qiling.exception import QlErrorArch, QlErrorSyscallError, QlErrorSyscallNotFound +from qiling.os.posix.kernel_proxy.ipc import ProxyClient +from qiling.os.posix.kernel_proxy.proxy_fd import ql_proxy_fd + +if TYPE_CHECKING: + from qiling import Qiling + + +class KernelProxy: + """Forward specific syscalls to a real Linux kernel via a helper process. + + The proxy process executes real syscalls and returns results. Integration + is through set_syscall() CALL hooks — no changes to Qiling's dispatch code. + """ + + def __init__(self, ql: Qiling): + if sys.platform != 'linux': + raise QlErrorArch("KernelProxy requires a Linux host") + + self.ql = ql + self._process: Optional[subprocess.Popen] = None + self._client: Optional[ProxyClient] = None + self._forwarded: Dict[str, int] = {} # name -> syscall_nr + self._reverse_table: Optional[Dict[str, int]] = None # name -> nr (built on first use) + + self._start_proxy() + + def _start_proxy(self): + """Start the proxy subprocess, connected via Unix socketpair.""" + parent_sock, child_sock = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) + + child_fd = child_sock.fileno() + + # ensure the subprocess can find qiling even when run from a subdirectory + env = os.environ.copy() + qiling_root = os.path.dirname(os.path.dirname(os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) + python_path = env.get('PYTHONPATH', '') + env['PYTHONPATH'] = f"{qiling_root}:{python_path}" if python_path else qiling_root + + self._process = subprocess.Popen( + [sys.executable, '-m', 'qiling.os.posix.kernel_proxy.proxy', str(child_fd)], + pass_fds=(child_fd,), + close_fds=True, + env=env, + ) + child_sock.close() + + self._client = ProxyClient(parent_sock) + self.ql.log.info(f"kernel proxy started (pid={self._process.pid})") + + def _build_reverse_table(self) -> Dict[str, int]: + """Build name -> syscall_nr mapping from the guest architecture's syscall table.""" + if self._reverse_table is not None: + return self._reverse_table + + from qiling.const import QL_ARCH + + # get the raw syscall table dict for this architecture + arch_tables = { + QL_ARCH.ARM64 : 'arm64_syscall_table', + QL_ARCH.ARM : 'arm_syscall_table', + QL_ARCH.X8664 : 'x8664_syscall_table', + QL_ARCH.X86 : 'x86_syscall_table', + QL_ARCH.MIPS : 'mips_syscall_table', + QL_ARCH.RISCV : 'riscv32_syscall_table', + QL_ARCH.RISCV64 : 'riscv64_syscall_table', + QL_ARCH.PPC : 'ppc_syscall_table', + } + + table_name = arch_tables.get(self.ql.arch.type) + if table_name is None: + raise QlErrorArch(f"KernelProxy: unsupported architecture {self.ql.arch.type}") + + import qiling.os.linux.map_syscall as mod + table = getattr(mod, table_name) + + # reverse: name -> nr + self._reverse_table = {name: nr for nr, name in table.items()} + return self._reverse_table + + def _resolve_syscall_nr(self, name: str) -> int: + """Resolve a syscall name to its number for the guest architecture.""" + table = self._build_reverse_table() + if name not in table: + raise QlErrorSyscallNotFound( + f"KernelProxy: syscall '{name}' not found in {self.ql.arch.type.name} syscall table" + ) + return table[name] + + def forward_syscall(self, name: str, returns_fd: bool = False): + """Register a CALL hook that forwards this syscall to the kernel proxy. + + Args: + name: syscall name (e.g. "epoll_create", "eventfd2") + returns_fd: if True, wrap the return value in ql_proxy_fd and store + in the Qiling FD table. Use this for syscalls that return + file descriptors (epoll_create, eventfd, timerfd_create, etc.) + """ + nr = self._resolve_syscall_nr(name) + self._forwarded[name] = nr + + forwarder = self._make_forwarder(name, nr, returns_fd) + self.ql.os.set_syscall(name, forwarder, QL_INTERCEPT.CALL) + + self.ql.log.info(f"forwarding syscall '{name}' (nr={nr}) to kernel proxy" + f"{' [returns FD]' if returns_fd else ''}") + + def _make_forwarder(self, name: str, guest_nr: int, returns_fd: bool): + """Create a CALL hook closure for one syscall.""" + client = self._client + + def _forwarder(ql, *args): + # use the HOST syscall number, not the guest number. + # for now, resolve from the host's syscall table at runtime. + host_nr = self._get_host_syscall_nr(name) + + padded = args + (0,) * (6 - len(args)) + retval = client.syscall(host_nr, padded[:6]) + + if returns_fd and retval >= 0: + # the proxy created a real FD. wrap it and store in Qiling's FD table. + proxy_fd_obj = ql_proxy_fd(client, retval) + guest_fd = self._alloc_fd(ql, proxy_fd_obj) + ql.log.debug(f"kernel_proxy: {name}() -> proxy_fd={retval}, guest_fd={guest_fd}") + return guest_fd + + ql.log.debug(f"kernel_proxy: {name}({', '.join(f'{a:#x}' for a in args)}) = {retval}") + return retval + + _forwarder.__name__ = f'ql_syscall_{name}' + return _forwarder + + def _get_host_syscall_nr(self, name: str) -> int: + """Get the syscall number on the HOST architecture.""" + # we are running on Linux — read from the host's syscall table + if not hasattr(self, '_host_table'): + self._host_table = self._load_host_syscall_table() + + if name not in self._host_table: + raise QlErrorSyscallNotFound(f"KernelProxy: syscall '{name}' not available on host") + + return self._host_table[name] + + def _load_host_syscall_table(self) -> Dict[str, int]: + """Load the host's syscall name->nr mapping. + + Uses the same Qiling tables, indexed by the host architecture. + """ + import platform + import qiling.os.linux.map_syscall as mod + + machine = platform.machine() + host_arch_map = { + 'x86_64': 'x8664_syscall_table', + 'aarch64': 'arm64_syscall_table', + 'armv7l': 'arm_syscall_table', + 'mips': 'mips_syscall_table', + 'riscv64': 'riscv64_syscall_table', + 'ppc': 'ppc_syscall_table', + } + + table_name = host_arch_map.get(machine) + if table_name is None: + raise QlErrorArch(f"KernelProxy: unsupported host architecture '{machine}'") + + table = getattr(mod, table_name) + return {name: nr for nr, name in table.items()} + + @staticmethod + def _alloc_fd(ql, fd_obj) -> int: + """Find next free slot in Qiling's FD table and store fd_obj.""" + for i in range(len(ql.os.fd)): + if ql.os.fd[i] is None: + ql.os.fd[i] = fd_obj + return i + + raise QlErrorSyscallError("kernel_proxy: FD table full") + + def stop(self): + """Stop the proxy process.""" + if self._client: + try: + self._client.close() + except Exception: + pass + self._client = None + + if self._process: + self._process.terminate() + try: + self._process.wait(timeout=5) + except subprocess.TimeoutExpired: + self._process.kill() + self._process.wait() + self.ql.log.info(f"kernel proxy stopped (pid={self._process.pid})") + self._process = None + + def __del__(self): + if hasattr(self, '_client'): + self.stop() diff --git a/qiling/os/posix/kernel_proxy/ipc.py b/qiling/os/posix/kernel_proxy/ipc.py new file mode 100644 index 000000000..59202c190 --- /dev/null +++ b/qiling/os/posix/kernel_proxy/ipc.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +""" +IPC protocol between Qiling and the kernel proxy process. + +Two message types: + SYSCALL — forward a raw syscall (number + 6 integer args) + FD_OP — perform an operation on a proxy-side FD (read/write/close/dup/fcntl/ioctl) + +All messages are length-prefixed binary over a Unix socketpair. +""" + +import struct +import socket +from enum import IntEnum + +from qiling.exception import QlProxyConnectionError + + +class MsgType(IntEnum): + SYSCALL = 1 + FD_OP = 2 + + +class FdOp(IntEnum): + READ = 1 + WRITE = 2 + CLOSE = 3 + DUP = 4 + FCNTL = 5 + IOCTL = 6 + + +# Wire format: +# Request header: [msg_type: u8][payload_len: u32] +# SYSCALL payload: [syscall_nr: u32][args: 6 x i64] +# FD_OP payload: [op: u8][proxy_fd: i32][arg1: i64][arg2: i64][data_len: u32][data: bytes] +# +# Response header: [status: i8][payload_len: u32] +# status 0 = success, -1 = error +# SYSCALL response payload: [return_value: i64][errno: i32] +# FD_OP response payload: [return_value: i64][errno: i32][data_len: u32][data: bytes] + +HEADER_FMT = '!BI' # msg_type/status (u8) + payload_len (u32) +HEADER_SIZE = struct.calcsize(HEADER_FMT) + +SYSCALL_REQ_FMT = '!I6q' # syscall_nr (u32) + 6 args (i64) +SYSCALL_REQ_SIZE = struct.calcsize(SYSCALL_REQ_FMT) + +SYSCALL_RESP_FMT = '!qi' # return_value (i64) + errno (i32) +SYSCALL_RESP_SIZE = struct.calcsize(SYSCALL_RESP_FMT) + +FD_OP_REQ_FMT = '!BiqqI' # op (u8) + proxy_fd (i32) + arg1 (i64) + arg2 (i64) + data_len (u32) +FD_OP_REQ_SIZE = struct.calcsize(FD_OP_REQ_FMT) + +FD_OP_RESP_FMT = '!qiI' # return_value (i64) + errno (i32) + data_len (u32) +FD_OP_RESP_SIZE = struct.calcsize(FD_OP_RESP_FMT) + + +def _recvall(sock: socket.socket, n: int) -> bytes: + """Receive exactly n bytes from a socket.""" + buf = bytearray() + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + raise QlProxyConnectionError("kernel proxy connection closed") + buf.extend(chunk) + return bytes(buf) + + +class ProxyClient: + """Qiling-side IPC client — sends requests to the proxy process.""" + + def __init__(self, sock: socket.socket): + self._sock = sock + + def syscall(self, nr: int, args: tuple) -> int: + """Forward a raw syscall. Returns kernel-convention result (negative errno on error).""" + padded = tuple(args) + (0,) * (6 - len(args)) + payload = struct.pack(SYSCALL_REQ_FMT, nr, *padded[:6]) + + # send request + header = struct.pack(HEADER_FMT, MsgType.SYSCALL, len(payload)) + self._sock.sendall(header + payload) + + # recv response + resp_header = _recvall(self._sock, HEADER_SIZE) + _, resp_len = struct.unpack(HEADER_FMT, resp_header) + resp_payload = _recvall(self._sock, resp_len) + + retval, errno_val = struct.unpack(SYSCALL_RESP_FMT, resp_payload) + return retval + + def _fd_op(self, op: FdOp, proxy_fd: int, arg1: int = 0, arg2: int = 0, data: bytes = b'') -> tuple: + """Send an FD operation. Returns (return_value, data).""" + payload = struct.pack(FD_OP_REQ_FMT, op, proxy_fd, arg1, arg2, len(data)) + payload += data + + header = struct.pack(HEADER_FMT, MsgType.FD_OP, len(payload)) + self._sock.sendall(header + payload) + + resp_header = _recvall(self._sock, HEADER_SIZE) + _, resp_len = struct.unpack(HEADER_FMT, resp_header) + resp_payload = _recvall(self._sock, resp_len) + + retval, errno_val, data_len = struct.unpack(FD_OP_RESP_FMT, resp_payload[:FD_OP_RESP_SIZE]) + resp_data = resp_payload[FD_OP_RESP_SIZE:FD_OP_RESP_SIZE + data_len] + + return retval, resp_data + + def fd_read(self, proxy_fd: int, length: int) -> bytes: + retval, data = self._fd_op(FdOp.READ, proxy_fd, arg1=length) + if retval < 0: + return b'' + return data + + def fd_write(self, proxy_fd: int, data: bytes) -> int: + retval, _ = self._fd_op(FdOp.WRITE, proxy_fd, data=data) + return retval + + def fd_close(self, proxy_fd: int) -> None: + self._fd_op(FdOp.CLOSE, proxy_fd) + + def fd_dup(self, proxy_fd: int) -> int: + retval, _ = self._fd_op(FdOp.DUP, proxy_fd) + return retval + + def fd_fcntl(self, proxy_fd: int, cmd: int, arg: int) -> int: + retval, _ = self._fd_op(FdOp.FCNTL, proxy_fd, arg1=cmd, arg2=arg) + return retval + + def fd_ioctl(self, proxy_fd: int, cmd: int, arg: int) -> int: + retval, _ = self._fd_op(FdOp.IOCTL, proxy_fd, arg1=cmd, arg2=arg) + return retval + + def close(self): + self._sock.close() + + +class ProxyServer: + """Proxy-side IPC server — receives requests, executes real syscalls.""" + + def __init__(self, sock: socket.socket): + self._sock = sock + + def recv_request(self) -> tuple: + """Receive one request. Returns (msg_type, parsed_fields).""" + header = _recvall(self._sock, HEADER_SIZE) + msg_type, payload_len = struct.unpack(HEADER_FMT, header) + + payload = _recvall(self._sock, payload_len) + + if msg_type == MsgType.SYSCALL: + fields = struct.unpack(SYSCALL_REQ_FMT, payload) + return MsgType.SYSCALL, fields # (nr, a0, a1, a2, a3, a4, a5) + + elif msg_type == MsgType.FD_OP: + fixed = struct.unpack(FD_OP_REQ_FMT, payload[:FD_OP_REQ_SIZE]) + op, proxy_fd, arg1, arg2, data_len = fixed + data = payload[FD_OP_REQ_SIZE:FD_OP_REQ_SIZE + data_len] + return MsgType.FD_OP, (FdOp(op), proxy_fd, arg1, arg2, data) + + else: + raise QlProxyConnectionError(f"unknown message type: {msg_type}") + + def send_syscall_response(self, retval: int, errno_val: int): + payload = struct.pack(SYSCALL_RESP_FMT, retval, errno_val) + header = struct.pack(HEADER_FMT, 0, len(payload)) + self._sock.sendall(header + payload) + + def send_fd_op_response(self, retval: int, errno_val: int, data: bytes = b''): + payload = struct.pack(FD_OP_RESP_FMT, retval, errno_val, len(data)) + payload += data + header = struct.pack(HEADER_FMT, 0, len(payload)) + self._sock.sendall(header + payload) diff --git a/qiling/os/posix/kernel_proxy/proxy.py b/qiling/os/posix/kernel_proxy/proxy.py new file mode 100644 index 000000000..af4934bd6 --- /dev/null +++ b/qiling/os/posix/kernel_proxy/proxy.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +""" +Kernel proxy process — executes real Linux syscalls on behalf of Qiling. + +This runs as a subprocess. It receives syscall requests over a Unix socketpair, +executes them via libc.syscall(), and sends back results. + +Usage (internal — started by KernelProxy.__init__): + python -m qiling.os.posix.kernel_proxy.proxy +""" + +import logging +import os +import sys +import ctypes +import ctypes.util +import errno as errno_module +import socket + +from qiling.os.posix.kernel_proxy.ipc import ( + ProxyServer, MsgType, FdOp +) + +log = logging.getLogger("qiling.os.posix.kernel_proxy.proxy") + +# load libc for raw syscall() +_libc_path = ctypes.util.find_library("c") +if _libc_path is None: + log.critical("kernel_proxy: cannot find libc") + sys.exit(1) + +_libc = ctypes.CDLL(_libc_path, use_errno=True) +_libc.syscall.restype = ctypes.c_long +_libc.syscall.argtypes = [ctypes.c_long] + [ctypes.c_long] * 6 + + +def raw_syscall(nr: int, a0: int, a1: int, a2: int, a3: int, a4: int, a5: int) -> tuple: + """Execute a real Linux syscall. Returns (return_value, errno) in kernel convention.""" + ctypes.set_errno(0) + result = _libc.syscall(nr, a0, a1, a2, a3, a4, a5) + + if result == -1: + err = ctypes.get_errno() + if err != 0: + return -err, err # kernel convention: negative errno + + return result, 0 + + +def handle_fd_op(op: FdOp, proxy_fd: int, arg1: int, arg2: int, data: bytes) -> tuple: + """Handle an FD operation on a proxy-side FD. Returns (retval, errno, data).""" + try: + if op == FdOp.READ: + result = os.read(proxy_fd, arg1) + return len(result), 0, result + + elif op == FdOp.WRITE: + written = os.write(proxy_fd, data) + return written, 0, b'' + + elif op == FdOp.CLOSE: + os.close(proxy_fd) + return 0, 0, b'' + + elif op == FdOp.DUP: + new_fd = os.dup(proxy_fd) + return new_fd, 0, b'' + + elif op == FdOp.FCNTL: + import fcntl + result = fcntl.fcntl(proxy_fd, arg1, arg2) + return result, 0, b'' + + elif op == FdOp.IOCTL: + import fcntl + result = fcntl.ioctl(proxy_fd, arg1, arg2) + return result, 0, b'' + + else: + return -errno_module.ENOSYS, errno_module.ENOSYS, b'' + + except OSError as e: + return -e.errno, e.errno, b'' + + +def main(): + if len(sys.argv) != 2: + log.error(f"usage: {sys.argv[0]} ") + sys.exit(1) + + sock_fd = int(sys.argv[1]) + sock = socket.socket(fileno=sock_fd) + server = ProxyServer(sock) + + while True: + try: + msg_type, fields = server.recv_request() + except ConnectionError: + break + + if msg_type == MsgType.SYSCALL: + nr, a0, a1, a2, a3, a4, a5 = fields + retval, err = raw_syscall(nr, a0, a1, a2, a3, a4, a5) + server.send_syscall_response(retval, err) + + elif msg_type == MsgType.FD_OP: + op, proxy_fd, arg1, arg2, data = fields + retval, err, resp_data = handle_fd_op(op, proxy_fd, arg1, arg2, data) + server.send_fd_op_response(retval, err, resp_data) + + sock.close() + + +if __name__ == '__main__': + main() diff --git a/qiling/os/posix/kernel_proxy/proxy_fd.py b/qiling/os/posix/kernel_proxy/proxy_fd.py new file mode 100644 index 000000000..cbd6d5c02 --- /dev/null +++ b/qiling/os/posix/kernel_proxy/proxy_fd.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +""" +Proxy file descriptor — wraps an FD that lives in the kernel proxy process. + +When a forwarded syscall returns an FD (e.g. epoll_create, eventfd), the real +FD lives in the proxy. This wrapper forwards read/write/close to the proxy via +IPC, matching the interface of ql_socket and ql_pipe so existing syscall handlers +(ql_syscall_read, ql_syscall_write, ql_syscall_close) work without modification. +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from qiling.os.posix.kernel_proxy.ipc import ProxyClient + + +class ql_proxy_fd: + def __init__(self, client: ProxyClient, proxy_fd: int): + self._client = client + self._proxy_fd = proxy_fd + + def read(self, length: int) -> bytes: + return self._client.fd_read(self._proxy_fd, length) + + def write(self, data: bytes) -> int: + return self._client.fd_write(self._proxy_fd, data) + + def close(self) -> None: + self._client.fd_close(self._proxy_fd) + + def fileno(self) -> int: + return -1 + + def dup(self) -> ql_proxy_fd: + new_proxy_fd = self._client.fd_dup(self._proxy_fd) + return ql_proxy_fd(self._client, new_proxy_fd) + + def fcntl(self, cmd, arg): + return self._client.fd_fcntl(self._proxy_fd, cmd, arg) + + def ioctl(self, cmd, arg): + return self._client.fd_ioctl(self._proxy_fd, cmd, arg) diff --git a/tests/test_kernel_proxy.py b/tests/test_kernel_proxy.py new file mode 100644 index 000000000..52495238c --- /dev/null +++ b/tests/test_kernel_proxy.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +import sys +import struct +import unittest +import platform + +sys.path.append("..") + +from qiling import Qiling +from qiling.const import QL_INTERCEPT, QL_VERBOSE + + +@unittest.skipUnless(platform.system() == 'Linux', 'kernel proxy requires Linux host') +class KernelProxyTest(unittest.TestCase): + """Tests for the hybrid kernel proxy (Phase 0).""" + + ROOTFS = "../examples/rootfs/x8664_linux" + HELLO_BIN = "../examples/rootfs/x8664_linux/bin/x8664_hello" + + # ------------------------------------------------------------------------- + # Proxy lifecycle + # ------------------------------------------------------------------------- + + def test_proxy_start_stop(self): + """Proxy starts and stops cleanly.""" + from qiling.os.posix.kernel_proxy import KernelProxy + + ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) + proxy = KernelProxy(ql) + + self.assertIsNotNone(proxy._process) + self.assertIsNotNone(proxy._client) + self.assertTrue(proxy._process.poll() is None) # still running + + proxy.stop() + self.assertIsNone(proxy._process) + self.assertIsNone(proxy._client) + + del ql + + def test_no_proxy_no_change(self): + """Without proxy, Qiling behaves identically.""" + from qiling.extensions import pipe + + ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) + ql.os.stdout = pipe.SimpleOutStream(1) + ql.run() + + self.assertIn(b"Hello", ql.os.stdout.read(1024)) + del ql + + def test_proxy_attached_no_forwarding(self): + """Proxy attached but no syscalls forwarded — no behavior change.""" + from qiling.os.posix.kernel_proxy import KernelProxy + from qiling.extensions import pipe + + ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) + ql.os.stdout = pipe.SimpleOutStream(1) + proxy = KernelProxy(ql) + + ql.run() + proxy.stop() + + self.assertIn(b"Hello", ql.os.stdout.read(1024)) + del ql + + # ------------------------------------------------------------------------- + # Raw syscall forwarding (integer-only args) + # ------------------------------------------------------------------------- + + def test_forward_getpid(self): + """Forward getpid — proxy returns its own PID.""" + from qiling.os.posix.kernel_proxy import KernelProxy + + ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) + proxy = KernelProxy(ql) + proxy.forward_syscall('getpid') + + results = [] + + def on_getpid_exit(ql, *args): + results.append(args[-1]) + + ql.os.set_syscall('getpid', on_getpid_exit, QL_INTERCEPT.EXIT) + ql.run() + + # if getpid was called, it should return the proxy's PID + if results: + self.assertEqual(results[0], proxy._process.pid) + + proxy.stop() + del ql + + def test_forward_brk(self): + """Forward brk to real kernel — binary still runs correctly.""" + from qiling.os.posix.kernel_proxy import KernelProxy + from qiling.extensions import pipe + + ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) + ql.os.stdout = pipe.SimpleOutStream(1) + proxy = KernelProxy(ql) + proxy.forward_syscall('brk') + + brk_results = [] + + def on_brk_exit(ql, *args): + brk_results.append(args[-1]) + + ql.os.set_syscall('brk', on_brk_exit, QL_INTERCEPT.EXIT) + ql.run() + proxy.stop() + + # brk is called during libc init + self.assertGreater(len(brk_results), 0, "brk was never called") + # binary should still produce correct output + self.assertIn(b"Hello", ql.os.stdout.read(1024)) + del ql + + # ------------------------------------------------------------------------- + # FD-returning syscalls (returns_fd=True) + # ------------------------------------------------------------------------- + + def test_forward_returns_fd(self): + """forward_syscall with returns_fd=True creates ql_proxy_fd in FD table.""" + from qiling.os.posix.kernel_proxy import KernelProxy + from qiling.os.posix.kernel_proxy.proxy_fd import ql_proxy_fd + + ql = Qiling([self.HELLO_BIN], self.ROOTFS, verbose=QL_VERBOSE.OFF) + proxy = KernelProxy(ql) + proxy.forward_syscall('eventfd2', returns_fd=True) + + # call the forwarder directly (simulates binary calling eventfd2) + hook = ql.os.posix_syscall_hooks[QL_INTERCEPT.CALL].get('ql_syscall_eventfd2') + self.assertIsNotNone(hook, "forwarder hook not registered") + + guest_fd = hook(ql, 0, 0) + self.assertGreaterEqual(guest_fd, 0) + + fd_obj = ql.os.fd[guest_fd] + self.assertIsInstance(fd_obj, ql_proxy_fd) + + # write and read through the proxy FD + fd_obj.write(struct.pack('