From d8146fbc77dfb538d31c167546392eb0185e8462 Mon Sep 17 00:00:00 2001 From: AllxSmr <127083562+AllxSmr@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:42:05 +0700 Subject: [PATCH] Add draft read-only DDR5 SPD5118 module and mock checks --- Ddr5ReadOnly.p | 156 +++++++++++++++++++++++++ tests/ddr5-readonly/.gitignore | 1 + tests/ddr5-readonly/CMakeLists.txt | 35 ++++++ tests/ddr5-readonly/README.md | 54 +++++++++ tests/ddr5-readonly/build.ps1 | 50 ++++++++ tests/ddr5-readonly/module-tests.c | 176 +++++++++++++++++++++++++++++ 6 files changed, 472 insertions(+) create mode 100644 Ddr5ReadOnly.p create mode 100644 tests/ddr5-readonly/.gitignore create mode 100644 tests/ddr5-readonly/CMakeLists.txt create mode 100644 tests/ddr5-readonly/README.md create mode 100644 tests/ddr5-readonly/build.ps1 create mode 100644 tests/ddr5-readonly/module-tests.c diff --git a/Ddr5ReadOnly.p b/Ddr5ReadOnly.p new file mode 100644 index 0000000..ff52b37 --- /dev/null +++ b/Ddr5ReadOnly.p @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2026 Hardware Tray contributors. +// Intel i801 register layout/transaction sequence adapted from PawnIO.Modules +// SmbusI801.p, Copyright (C) 2025 Steve-Tech, LGPL-2.1-or-later, commit +// 52a7e536dff3e53c96917a28caac5e0fa6510696. See COPYING in that project. +// Candidate module: NOT signed, NOT approved for installation on real hardware. +#include + +new VA:controller; +new pci_function = -1; +bool:allowed_read(address, direction, register, protocol); +NTSTATUS:read_register(address, register, protocol, &result); + +// This is the only exported hardware operation. There is no generic SMBus, +// EEPROM, MSR, PCI-write, physical-memory-write or module-loading interface. +// Check full-width cells before any truncation or hardware/native operation. +/// Read one allowlisted DDR5 SPD5118 live register through an Intel i801. +/// @param in [address, direction (=1), register, protocol (=2 byte / =3 word)] +/// @param in_size Exactly four 64-bit cells; write payloads are rejected. +/// @param out One zero-extended byte/word; valid only on STATUS_SUCCESS. +/// @param out_size Exactly one 64-bit cell. +/// @return STATUS_ACCESS_DENIED for non-read/allowlist violations. +/// @warning Hold the global Access_SMBUS.HTP.Method mutex for the entire scan. +DEFINE_IOCTL(ioctl_ddr5_read) { + if (in_size != 4 || out_size != 1) + return STATUS_INVALID_PARAMETER; + if (!allowed_read(in[0], in[1], in[2], in[3])) + return STATUS_ACCESS_DENIED; + out[0] = 0; + return read_register(in[0], in[2], in[3], out[0]); +} + +bool:allowed_read(address, direction, register, protocol) { + if (address < 0x50 || address > 0x57 || direction != 1) + return false; + switch (register) { + case 0x00, 0x03, 0x31: return protocol == 3; + case 0x05, 0x0b, 0x1a: return protocol == 2; + } + return false; +} + +NTSTATUS:main() { + // Intel client platforms only, bus 0 device 31 function 4 or 3. + // No PCI writes: disabled controller, I2C mode or memory decode => unavailable. + new value; + for (new function = 4; function >= 3; --function) { + if (!NT_SUCCESS(pci_config_read_word(0, 31, function, 0, value)) || value != 0x8086) + continue; + if (!NT_SUCCESS(pci_config_read_word(0, 31, function, 0x0a, value)) || value != 0x0c05) + continue; + pci_function = function; + break; + } + if (pci_function == -1) + return STATUS_NOT_SUPPORTED; + if (!NT_SUCCESS(pci_config_read_word(0, 31, pci_function, 4, value)) || (value & 2) == 0) + return STATUS_NOT_SUPPORTED; + if (!NT_SUCCESS(pci_config_read_byte(0, 31, pci_function, 0x40, value)) || (value & 5) != 1) + return STATUS_NOT_SUPPORTED; + if (!NT_SUCCESS(pci_config_read_qword(0, 31, pci_function, 0x10, value)) || (value & 1) != 0) + return STATUS_NOT_SUPPORTED; + new bar_type = value & 6; + if (bar_type == 0) + value &= 0xffffffff; + else if (bar_type != 4) + return STATUS_NOT_SUPPORTED; + value &= 0xffffffffffffff00; + if (value == 0) + return STATUS_NOT_SUPPORTED; + controller = io_space_map(value, 0x18); + return controller == NULL ? STATUS_INSUFFICIENT_RESOURCES : STATUS_SUCCESS; +} + +public NTSTATUS:unload() { + if (controller != NULL) { + io_space_unmap(controller, 0x18); + controller = NULL; + } + return STATUS_SUCCESS; +} + +NTSTATUS:read_register(address, register, protocol, &result) { + // Defense in depth: even internal callers can request only allowlisted reads. + if (!allowed_read(address, 1, register, protocol)) + return STATUS_ACCESS_DENIED; + if (controller == NULL) + return STATUS_DEVICE_NOT_READY; + new config; + if (!NT_SUCCESS(pci_config_read_word(0, 31, pci_function, 4, config)) || (config & 2) == 0) + return STATUS_NOT_SUPPORTED; + if (!NT_SUCCESS(pci_config_read_byte(0, 31, pci_function, 0x40, config)) || (config & 5) != 1) + return STATUS_NOT_SUPPORTED; + + new host_status, old_control, auxiliary; + new NTSTATUS:status = virtual_read_byte(controller, host_status); + if (!NT_SUCCESS(status)) return status; + if (host_status & 0x40) // Already INUSE: the claim belongs to another client. + return STATUS_DEVICE_BUSY; + if (host_status & 1) { + // Our status read acquired INUSE, but a transaction was already running. + // Release only our claim, preserving the other transaction's status flags. + virtual_write_byte(controller, 0x40); + return STATUS_DEVICE_BUSY; + } + + // Reading INUSE_STS claims the controller on i801. Release only after our claim. + status = virtual_read_byte(controller + 2, old_control); + if (!NT_SUCCESS(status)) goto release; + if (old_control & 0x42) { status = STATUS_DEVICE_BUSY; goto release; } + status = virtual_read_byte(controller + 13, auxiliary); + if (!NT_SUCCESS(status)) goto release; + // Do not change controller PEC/block-buffer settings to make a read possible. + if (auxiliary & 3) { status = STATUS_NOT_SUPPORTED; goto release; } + status = virtual_write_byte(controller, host_status & 0x9e); + if (!NT_SUCCESS(status)) goto release; + // Always set the bus direction bit to READ. No data-byte write exists here. + status = virtual_write_byte(controller + 4, (address << 1) | 1); + if (!NT_SUCCESS(status)) goto release; + status = virtual_write_byte(controller + 3, register); + if (!NT_SUCCESS(status)) goto release; + status = virtual_write_byte(controller + 2, (protocol == 3 ? 0x0c : 0x08) | 0x40); + if (!NT_SUCCESS(status)) goto restore; + + new deadline = get_tick_count() + 80; + new attempts = 0; + do { + microsleep(100); + status = virtual_read_byte(controller, host_status); + if (!NT_SUCCESS(status)) goto stop_own; + if ((host_status & 1) == 0 && (host_status & 0x1e) != 0) + break; + } while (++attempts < 800 && get_tick_count() < deadline); + if ((host_status & 1) || (host_status & 0x1e) == 0) { + status = STATUS_IO_TIMEOUT; + goto stop_own; + } + if (host_status & 0x1c) { status = STATUS_IO_DEVICE_ERROR; goto restore; } + if (protocol == 3) + status = virtual_read_word(controller + 5, result); + else + status = virtual_read_byte(controller + 5, result); + goto restore; + +stop_own: + // Abort only the transaction started above; never used on initial busy state. + virtual_write_byte(controller + 2, 2); + microsleep(1000); + virtual_write_byte(controller + 2, 0); +restore: + virtual_write_byte(controller + 2, old_control); +release: + // Controller handshake/status registers only, not peripheral configuration. + virtual_write_byte(controller, 0xde); + return status; +} diff --git a/tests/ddr5-readonly/.gitignore b/tests/ddr5-readonly/.gitignore new file mode 100644 index 0000000..30bcfa4 --- /dev/null +++ b/tests/ddr5-readonly/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/tests/ddr5-readonly/CMakeLists.txt b/tests/ddr5-readonly/CMakeLists.txt new file mode 100644 index 0000000..978cd18 --- /dev/null +++ b/tests/ddr5-readonly/CMakeLists.txt @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +cmake_minimum_required(VERSION 3.15) +project(Ddr5ReadOnlyMockChecks C) +set(CMAKE_C_STANDARD 99) +if(NOT EXISTS "${PAWN_SOURCE}/compiler/sc1.c") + message(FATAL_ERROR "PAWN_SOURCE must reference the verified Pawn 4.1.7152 source archive") +endif() +set(cc_names sc1 sc2 sc3 sc4 sc5 sc6 sc7 scexpand sci18n sclist scmemfil scstate scvars lstring memfile) +set(cc_sources "${PAWN_SOURCE}/amx/keeloq.c") +foreach(name IN LISTS cc_names) + if(name STREQUAL "sc3") + # Legacy declarations qualify void return types, incompatible with modern GCC. + file(READ "${PAWN_SOURCE}/compiler/sc3.c" sc3_source) + string(REPLACE "const void (*" "void (*" sc3_source "${sc3_source}") + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/sc3-compatible.c" "${sc3_source}") + list(APPEND cc_sources "${CMAKE_CURRENT_BINARY_DIR}/sc3-compatible.c") + else() + list(APPEND cc_sources "${PAWN_SOURCE}/compiler/${name}.c") + endif() +endforeach() +# No installer/IDE/icon resources; only the compiler and a user-mode mock host. +add_executable(pawncc ${cc_sources}) +target_include_directories(pawncc PRIVATE "${PAWN_SOURCE}/compiler") +target_compile_definitions(pawncc PRIVATE HAVE_INTTYPES_H HAVE_STDINT_H HAVE_UNISTD_H HAVE_ALLOCA_H=0) +# Upstream's 64-bit VM assumes LP64 unsigned long. Windows is LLP64. +# Generate a local test-host copy with cell-width shifts; never alter module bytecode. +file(READ "${PAWN_SOURCE}/amx/amx.c" amx_source) +string(REPLACE "1UL << sizeof(cell)*4" "((ucell)1) << sizeof(cell)*4" amx_source "${amx_source}") +string(REPLACE "1L<<(sizeof(cell)*4)" "((ucell)1)<<(sizeof(cell)*4)" amx_source "${amx_source}") +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/amx-llp64.c" "${amx_source}") +add_executable(readonly-module-tests module-tests.c + "${CMAKE_CURRENT_BINARY_DIR}/amx-llp64.c" "${PAWN_SOURCE}/amx/amxaux.c") +target_include_directories(readonly-module-tests PRIVATE "${PAWN_SOURCE}/amx") +target_compile_definitions(readonly-module-tests PRIVATE + PAWN_CELL_SIZE=64 AMX_NODYNALOAD AMX_ANSIONLY HAVE_INTTYPES_H HAVE_STDINT_H HAVE_ALLOCA_H=0) diff --git a/tests/ddr5-readonly/README.md b/tests/ddr5-readonly/README.md new file mode 100644 index 0000000..9af7fc5 --- /dev/null +++ b/tests/ddr5-readonly/README.md @@ -0,0 +1,54 @@ +# DDR5 read-only module: mock checks + +This is an **unsigned, unvalidated hardware candidate**, not an installation recommendation. The checks execute the actual compiled `Ddr5ReadOnly.p` bytecode in a standalone 64-bit-cell Pawn VM. Every hardware native is a mock. No driver is loaded, and no hardware is accessed. + +## Contract + +The only hardware export, `ioctl_ddr5_read`, takes exactly four cells +`[address, direction, register, protocol]` and exactly one output cell. + +| Address | Direction | Registers | Protocol | +| --- | --- | --- | --- | +| `0x50..0x57` | `1` (read) | `0x00`, `0x03`, `0x31` | `3` (word) | +| `0x50..0x57` | `1` (read) | `0x05`, `0x0b`, `0x1a` | `2` (byte) | + +Validation uses full-width cells before any hardware native call. There is no generic SMBus transfer export or caller-supplied write payload. A client should check SPD5118 identity, capabilities, page and sensor state before interpreting the raw temperature register. This module does not identify individual DIMMs or convert temperatures for the caller. + +"Read-only" describes peripheral transactions, not an absence of controller writes: the transport necessarily writes i801 address/command/control/status registers to issue a read and release its claim. It does not write peripheral configuration/data, change the SPD page, enable a sensor/controller, or write PCI configuration. The intended policy is enforced by module code executing inside PawnIO's kernel interpreter, rather than by trusting client-side validation. + +This restriction applies to **this module's exported operations only**. Stock PawnIO may load other signed modules with broader capabilities. This is not a dedicated read-only driver or a system-wide write prohibition, and it does not protect against an administrator using a different driver/module. + +## Reproduce on Windows without a driver + +Prerequisites: PowerShell and an existing MinGW GCC/CMake toolchain. From the repository root: + +```powershell +./tests/ddr5-readonly/build.ps1 +# If the toolchain is not on PATH: +./tests/ddr5-readonly/build.ps1 -MinGwBin 'C:\toolchains\mingw64\bin' +# Reuse the verified local source archive without network access: +./tests/ddr5-readonly/build.ps1 -Offline +``` + +The script uses this checkout's `include/` and verifies the upstream Pawn 4.1.7152 source ZIP SHA256 before extraction. Build/cache output stays in `tests/ddr5-readonly/.build/` by default; `-CachePath` can select a dedicated cache directory. Nothing is installed, signed or deployed. The unsigned AMX hash is printed. + +The CMake harness generates two compatibility corrections in its build directory: ineffective `const` on legacy void-return function-pointer declarations, and cell-width shifts for the 64-bit VM on Windows LLP64. It does not patch module bytecode. These checks are separate from the repository's standard Linux compiler CI; no claim of CI success is made by this script. + +Checks cover 65,585 requests, including the 7-bit address/8-bit register space in read/write directions, invalid full-width values, mismatched buffer lengths/protocols, already-owned/busy controllers, disabled PCI decoding and a bounded transaction timeout. The harness checks the compiled public/native allowlists and requires denied requests to reach no hardware native. Allowed writes are checked against fixed controller offsets, command values and the forced read-direction bit. + +## Outstanding acceptance work + +- No execution in the Windows kernel or on actual hardware has been performed. +- Compatibility is not established by PCI vendor/class matching: current discovery is limited to Intel bus 0, device 31, function 4/3 with MMIO decoding and SMBus host mode already enabled. +- Real MMIO ordering, controller ownership/ACPI concurrency, read/abort failure cleanup, firmware interaction and suspend/resume need review and hardware validation. +- The caller must hold the shared `Access_SMBUS.HTP.Method` mutex; that is not protection from uncooperative firmware or other clients. +- Mock success does not prove kernel safety, correct readings, complete fault handling or universal hardware support. Do not test this candidate on a daily-use machine. + +## References and licensing + +- [PawnIO.Modules contribution requirements](https://github.com/namazso/PawnIO.Modules/wiki/Contribution-guidelines). +- [Upstream i801 transport reference, pinned base](https://github.com/namazso/PawnIO.Modules/blob/52a7e536dff3e53c96917a28caac5e0fa6510696/SmbusI801.p), copyright Steve-Tech, LGPL-2.1-or-later. Attribution is retained in the new module. +- [Linux SPD5118 protocol reference](https://github.com/torvalds/linux/blob/master/drivers/hwmon/spd5118.c), used as a register/protocol reference, not copied into this implementation. +- [Pawn compiler source](https://www.compuphase.com/pawn/pawn-4.1.7152.zip), pinned by SHA256 in the script; original upstream notices remain in the downloaded source. + +The added module and mock-harness code are LGPL-2.1-or-later; see the repository's `COPYING`. Acceptance, any requested hardware testing, signing and release remain upstream decisions. diff --git a/tests/ddr5-readonly/build.ps1 b/tests/ddr5-readonly/build.ps1 new file mode 100644 index 0000000..4709d3c --- /dev/null +++ b/tests/ddr5-readonly/build.ps1 @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[CmdletBinding()] +param([string]$MinGwBin, [string]$CachePath, [switch]$Offline) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$testRoot = $PSScriptRoot +$repoRoot = Split-Path -Parent (Split-Path -Parent $testRoot) +if (-not $CachePath) { $CachePath = Join-Path $testRoot '.build' } +$CachePath = [IO.Path]::GetFullPath($CachePath) +New-Item -ItemType Directory -Force -Path $CachePath | Out-Null +if (-not $MinGwBin) { $MinGwBin = Split-Path -Parent (Get-Command cmake -ErrorAction Stop).Source } +$testCmake = Join-Path $MinGwBin 'cmake.exe' +$testGcc = Join-Path $MinGwBin 'gcc.exe' +$testMake = Join-Path $MinGwBin 'mingw32-make.exe' +foreach ($testTool in @($testCmake, $testGcc, $testMake)) { + if (-not (Test-Path -LiteralPath $testTool -PathType Leaf)) { throw "Missing tool: $testTool" } +} +$testInclude = Join-Path $repoRoot 'include' +if (-not (Test-Path -LiteralPath (Join-Path $testInclude 'pawnio.inc'))) { + throw 'Run from a PawnIO.Modules checkout containing include/pawnio.inc.' +} +$testArchive = Join-Path $CachePath 'pawn-4.1.7152.zip' +if (-not (Test-Path -LiteralPath $testArchive)) { + if ($Offline) { throw 'Offline dependency missing: pawn-4.1.7152.zip' } + Invoke-WebRequest -Uri 'https://www.compuphase.com/pawn/pawn-4.1.7152.zip' -OutFile $testArchive +} +if ((Get-FileHash -LiteralPath $testArchive -Algorithm SHA256).Hash -ne + 'C2E7212098A68AD1CBACC8B5CFE44A7E6F95B75A82D1E5D43CCF9330F991A87B') { + throw 'Pawn source archive hash mismatch.' +} +$testSource = Join-Path $CachePath 'pawn-7152-source' +Expand-Archive -LiteralPath $testArchive -DestinationPath $testSource -Force +$testBuild = Join-Path $CachePath 'ddr5-readonly-mock-build' +& $testCmake -S $testRoot -B $testBuild -G 'MinGW Makefiles' ` + "-DPAWN_SOURCE=$testSource" '-DCMAKE_BUILD_TYPE=Debug' "-DCMAKE_C_COMPILER=$testGcc" "-DCMAKE_MAKE_PROGRAM=$testMake" +if ($LASTEXITCODE -ne 0) { throw 'Mock toolchain configure failed.' } +& $testCmake --build $testBuild --parallel 2 +if ($LASTEXITCODE -ne 0) { throw 'Mock toolchain build failed.' } +$testBinary = Join-Path $testBuild 'Ddr5ReadOnly.amx' +$testCompilerOutput = & (Join-Path $testBuild 'pawncc.exe') (Join-Path $repoRoot 'Ddr5ReadOnly.p') ` + '-C64' '-;+' '-(+' '-p' "-i$testInclude" "-o$testBinary" 2>&1 +$testCompileExit = $LASTEXITCODE +$testCompilerOutput | ForEach-Object { Write-Host $_ } +if ($testCompileExit -ne 0 -or "$testCompilerOutput" -match '(?i)warning\s+\d+|error\s+\d+') { + throw 'The module must compile without warnings or errors.' +} +& (Join-Path $testBuild 'readonly-module-tests.exe') $testBinary +if ($LASTEXITCODE -ne 0) { throw 'Compiled-module mock tests failed.' } +Write-Host "Unsigned AMX SHA256: $((Get-FileHash -LiteralPath $testBinary -Algorithm SHA256).Hash)" +Write-Host 'User-mode mocks only. No driver installation, kernel execution or real-hardware validation.' diff --git a/tests/ddr5-readonly/module-tests.c b/tests/ddr5-readonly/module-tests.c new file mode 100644 index 0000000..9fe3933 --- /dev/null +++ b/tests/ddr5-readonly/module-tests.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2026 Hardware Tray contributors. +// Executes the UNMODIFIED compiled production module in a user-mode Pawn VM. +// All hardware natives below are mocks. No Windows driver or hardware access. +#include +#include +#include +#include +#include "amx.h" +#include "amxaux.h" + +static int native_calls, writes, started, starts, initial_busy, force_timeout, decode_enabled = 1, cases; +static cell bus_address, command, control, ticks; +static void require(int ok, const char *message) { + if (!ok) { fprintf(stderr, "FAIL %s (case %d)\n", message, cases); exit(1); } +} +static cell AMX_NATIVE_CALL pci_read(AMX *vm, const cell *p) { + native_calls++; + require(p[1] == 0 && p[2] == 31 && (p[3] == 3 || p[3] == 4), "unexpected PCI location"); + cell value = 0; + switch (p[4]) { + case 0: value = 0x8086; break; + case 0x0a: value = 0x0c05; break; + case 4: value = decode_enabled ? 2 : 0; break; + case 0x40: value = 1; break; + case 0x10: value = 0x100000; break; + default: require(0, "unexpected PCI register"); + } + *amx_Address(vm, p[5]) = value; + return 0; +} +static cell AMX_NATIVE_CALL map_io(AMX *vm, const cell *p) { + (void)vm; native_calls++; + require(p[1] == 0x100000 && p[2] == 0x18, "unexpected mapping"); + return 0x200000; +} +static cell AMX_NATIVE_CALL unmap_io(AMX *vm, const cell *p) { + (void)vm; native_calls++; require(p[1] == 0x200000 && p[2] == 0x18, "unexpected unmap"); return 0; +} +static cell AMX_NATIVE_CALL read_io(AMX *vm, const cell *p) { + native_calls++; + cell value = 0; + switch (p[1] - 0x200000) { + case 0: value = initial_busy ? initial_busy : started ? (force_timeout ? 1 : 2) : 0; break; + case 2: value = control; break; + case 13: value = 0; break; + case 5: value = command == 0x31 ? 0x360 : 0; break; + default: require(0, "unexpected MMIO read"); + } + *amx_Address(vm, p[2]) = value; + return 0; +} +static cell AMX_NATIVE_CALL write_io(AMX *vm, const cell *p) { + (void)vm; native_calls++; writes++; + switch (p[1] - 0x200000) { + case 0: require((p[2] & ~0xde) == 0, "unexpected host status write"); break; + case 2: + control = p[2]; + require(control == 0 || control == 2 || control == 0x48 || control == 0x4c, + "unexpected host transaction type"); + if (control & 0x40) { + require((bus_address & 1) == 1, "PERIPHERAL WRITE transaction attempted"); + require(bus_address >= 0xa1 && bus_address <= 0xaf, "address escaped DIMM range"); + started = 1; starts++; + } else started = 0; + break; + case 3: + command = p[2]; + require(command == 0 || command == 3 || command == 5 || command == 0x0b || + command == 0x1a || command == 0x31, "register escaped allowlist"); + break; + case 4: + bus_address = p[2]; + require((bus_address & 1) == 1, "write direction reached hardware native"); + break; + default: require(0, "MMIO write outside controller command/handshake registers"); + } + return 0; +} +static cell AMX_NATIVE_CALL shared_tick(AMX *vm, const cell *p) { + native_calls++; + if ((ucell)p[1] == 0xfffff78000000320ULL) *amx_Address(vm, p[2]) = ticks++; + else if ((ucell)p[1] == 0xfffff78000000004ULL) *amx_Address(vm, p[2]) = 1 << 24; + else require(0, "unexpected shared data address"); + return 0; +} +static cell AMX_NATIVE_CALL pause_us(AMX *vm, const cell *p) { + (void)vm; native_calls++; require(p[1] >= 0 && p[1] <= 1000, "unbounded wait"); return 0; +} +static const AMX_NATIVE_INFO natives[] = { + {"pci_config_read_word", pci_read}, {"pci_config_read_byte", pci_read}, {"pci_config_read_qword", pci_read}, + {"io_space_map", map_io}, {"io_space_unmap", unmap_io}, + {"virtual_read_byte", read_io}, {"virtual_read_word", read_io}, {"virtual_write_byte", write_io}, + {"virtual_read_qword", shared_tick}, {"virtual_read_dword", shared_tick}, + {"microsleep", pause_us}, {NULL, NULL} +}; +static void call(AMX *vm, int function, cell address, cell direction, cell reg, cell protocol, + cell in_size, cell out_size, int allowed) { + cell input[5] = {address, direction, reg, protocol, 0x1234}, output[2] = {0, 0}, result; + cell *out_ptr; + native_calls = writes = starts = 0; + require(amx_Push(vm, out_size) == 0, "push output length"); + require(amx_PushArray(vm, &out_ptr, output, 2) == 0, "push output"); + require(amx_Push(vm, in_size) == 0, "push input length"); + require(amx_PushArray(vm, NULL, input, 5) == 0, "push input"); + require(amx_Exec(vm, &result, function) == 0, "VM execution"); + cases++; + if (allowed == 1) require(result == 0 && starts == 1, "allowlisted read did not execute once"); + else if (allowed == -1) require(result < 0 && writes == 0, "unavailable controller was changed"); + else if (allowed == -2) require(result < 0 && starts == 1 && native_calls < 4000, + "own transaction timeout did not terminate within bounds"); + else if (allowed == -3) require(result < 0 && writes == 1 && starts == 0, + "busy transaction was disturbed instead of releasing only our claim"); + else { + require(result < 0, "forbidden request accepted"); + require(native_calls == 0, "forbidden request touched a hardware native"); + } + require(amx_Release(vm, out_ptr) == 0, "release VM buffers"); +} +int main(int argc, char **argv) { + require(argc == 2, "supply compiled module path"); + AMX vm; int function, count; cell result; + require(aux_LoadProgram(&vm, argv[1], NULL) == 0, "load compiled AMX"); + require(amx_NumNatives(&vm, &count) == 0, "native inventory"); + for (int i = 0; i < count; i++) { + char name[128]; int found = 0; + require(amx_GetNative(&vm, i, name) == 0, "native name"); + for (int j = 0; natives[j].name; j++) if (!strcmp(name, natives[j].name)) found = 1; + if (!found) fprintf(stderr, "Unexpected native: %s\n", name); + require(found, "module imports unexpected native capability"); + } + require(amx_NumPublics(&vm, &count) == 0 && count == 2, "unexpected exported interface"); + for (int i = 0; i < count; i++) { + char name[128]; ucell address; + require(amx_GetPublic(&vm, i, name, &address) == 0, "public name"); + require(!strcmp(name, "ioctl_ddr5_read") || !strcmp(name, "unload"), "unexpected export"); + } + require(amx_Register(&vm, natives, -1) == 0, "bind mocks only"); + require(amx_Exec(&vm, &result, AMX_EXEC_MAIN) == 0 && result == 0, "module initialization"); + require(amx_FindPublic(&vm, "ioctl_ddr5_read", &function) == 0, "read interface missing"); + for (int address = 0; address < 128; address++) + for (int reg = 0; reg < 256; reg++) { + int word = reg == 0 || reg == 3 || reg == 0x31; + int byte = reg == 5 || reg == 0x0b || reg == 0x1a; + int allowed = address >= 0x50 && address <= 0x57 && (word || byte); + call(&vm, function, address, 1, reg, word ? 3 : 2, 4, 1, allowed); + call(&vm, function, address, 0, reg, word ? 3 : 2, 4, 1, 0); + } + cell extremes[] = {-1, 2, 0x100000001LL, INT64_MIN, INT64_MAX}; + for (unsigned i = 0; i < sizeof extremes / sizeof extremes[0]; i++) { + call(&vm, function, 0x50, extremes[i], 0x31, 3, 4, 1, 0); + call(&vm, function, extremes[i], 1, 0x31, 3, 4, 1, 0); + call(&vm, function, 0x50, 1, extremes[i], 3, 4, 1, 0); + } + for (int size = -1; size <= 9; size++) { + if (size != 4) call(&vm, function, 0x50, 1, 0x31, 3, size, 1, 0); + if (size != 1) call(&vm, function, 0x50, 1, 0x31, 3, 4, size, 0); + if (size != 3) call(&vm, function, 0x50, 1, 0x31, size, 4, 1, 0); + } + initial_busy = 0x41; + call(&vm, function, 0x50, 1, 0x31, 3, 4, 1, -1); + initial_busy = 1; + call(&vm, function, 0x50, 1, 0x31, 3, 4, 1, -3); + initial_busy = 0; decode_enabled = 0; + call(&vm, function, 0x50, 1, 0x31, 3, 4, 1, -1); + decode_enabled = 1; force_timeout = 1; + call(&vm, function, 0x50, 1, 0x31, 3, 4, 1, -2); + force_timeout = 0; + require(amx_FindPublic(&vm, "ioctl_smbus_xfer", &function) != 0, "generic SMBus interface exists"); + require(amx_FindPublic(&vm, "unload", &function) == 0, "unload missing"); + require(amx_Exec(&vm, &result, function) == 0 && result == 0, "unload failed"); + aux_FreeProgram(&vm); + printf("PASS %d compiled-module request cases; no forbidden request reached a hardware native.\n", cases); + puts("PASS export/native allowlists. User-mode mocked hardware only; not a kernel/hardware acceptance test."); + return 0; +}