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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions Ddr5ReadOnly.p
Original file line number Diff line number Diff line change
@@ -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 <pawnio.inc>

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;
}
1 change: 1 addition & 0 deletions tests/ddr5-readonly/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.build/
35 changes: 35 additions & 0 deletions tests/ddr5-readonly/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
54 changes: 54 additions & 0 deletions tests/ddr5-readonly/README.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions tests/ddr5-readonly/build.ps1
Original file line number Diff line number Diff line change
@@ -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.'
Loading