From 8fffbc2ec9cf6900161cf3f25fbdd63de5d4c6f1 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 10:44:09 +0100 Subject: [PATCH 01/17] tools: add SP11 driver metadata and camera resource research tools --- tools/sp11-driver-audit/README.md | 96 +++++++++ tools/sp11-driver-audit/aeob.mjs | 186 ++++++++++++++++++ tools/sp11-driver-audit/aeob.test.mjs | 141 +++++++++++++ tools/sp11-driver-audit/camera-config.mjs | 56 ++++++ .../sp11-driver-audit/camera-config.test.mjs | 57 ++++++ tools/sp11-driver-audit/camera-fields.md | 76 +++++++ tools/sp11-driver-audit/inventory.mjs | 129 ++++++++++++ tools/sp11-driver-audit/inventory.test.mjs | 48 +++++ 8 files changed, 789 insertions(+) create mode 100644 tools/sp11-driver-audit/README.md create mode 100644 tools/sp11-driver-audit/aeob.mjs create mode 100644 tools/sp11-driver-audit/aeob.test.mjs create mode 100644 tools/sp11-driver-audit/camera-config.mjs create mode 100644 tools/sp11-driver-audit/camera-config.test.mjs create mode 100644 tools/sp11-driver-audit/camera-fields.md create mode 100644 tools/sp11-driver-audit/inventory.mjs create mode 100644 tools/sp11-driver-audit/inventory.test.mjs diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md new file mode 100644 index 00000000000000..cb1b3c82f9bc2c --- /dev/null +++ b/tools/sp11-driver-audit/README.md @@ -0,0 +1,96 @@ +# Surface Pro 11 driver research + +These independently written Linux development tools inventory an extracted +Surface driver package and decode selected camera resource metadata. They are +the first source deliverable in the SP11 parity work. They do not install +drivers, execute Windows programs, access hardware, generate device trees, or +establish that a device is present. + +The intended production result is native Linux kernel and userspace source +that builds and runs without Windows or the MSI. Windows package inspection is +an optional research step, outside that production dependency chain. Device +firmware is a separate dependency whose Linux availability and device-specific +authentication must be established. + +## Run + +Node.js 22 or newer is sufficient for these tools; no npm dependencies are +required. Use an extracted package you already have: + +```sh +node tools/sp11-driver-audit/inventory.mjs /path/to/SurfaceUpdate > inventory.json +node tools/sp11-driver-audit/aeob.mjs --input /path/to/CAMS_RES_MSHW0491.bin +node tools/sp11-driver-audit/camera-config.mjs \ + --kind prld --input /path/to/CAMP_PRLD_MSHW0495.bin +node tools/sp11-driver-audit/camera-config.mjs \ + --kind pcfg --input /path/to/CAMP_PCFG_MSHW0495.bin +node --test tools/sp11-driver-audit/*.test.mjs +``` + +The inventory includes SHA-256 identities, file sizes, PE architecture, INF +versions, manufacturer-declared model bindings, service declarations and source +file declarations. Model identities are package declarations; subsequent IDs +in a model row are compatible IDs, not additional proof of installed hardware. +Service declarations include sections that might never be selected. Broken +string declarations remain unresolved and produce warnings. Child symbolic +links are skipped. INF path strings are never opened as commands or files. + +The AeoB reader checks lengths, top-level counts, nested boundaries, integer +precision and resource limits. It recognizes the observed integer, string and +nested tuple forms. Unknown tags retain a digest and byte range without a +guessed interpretation. Unsupported layouts fail explicitly. Generated JSON +is research data and should remain outside the kernel source tree. + +The camera interpreter uses the [recorded field specification](camera-fields.md). +It preserves both platform configuration banks, raw values and unknown tail +fields. It does not select a board, turn orientation codes into angles, or +translate Windows resource indices into Linux GPIO or CCI numbers. + +## Initial measured coverage + +The April 2026 package named +`SurfacePro11_ARM_Win11_26100_26.041.12746.0` contains 1,126 files and 205 INF +files. The inventory retains bindings from every INF, with two malformed vendor +string declarations reported as warnings. Static PE headers identify 190 ARM64 +and 17 I386 objects; the package also contains DSP and firmware objects in other +formats. + +Of 72 files beginning with `AeoB`, 71 follow the observed layout, containing +46,033 records with maximum nesting depth six. `qcadc/ADC1.bin` uses an +unsupported layout and is rejected at offset 253. This is a coverage limit, +not a claim that the vendor file is corrupt or that all AeoB variants match. + +## Implementation tracks + +| Track | Existing Linux position | Next implementation evidence | +| --- | --- | --- | +| Front IMX681 | Native driver and OLED graph; prior preview and indicator tests | Compare verified power, lane and stream/error lifecycle with this package | +| Rear OV13858 | Native sensor driver assumes an ACPI-powered device; no Denali graph | Native power resources, module-specific PHY/modes, actuator and indicator ownership | +| IR VD55G0/VD55G1 | Different sensors and board variants; support differs | Exact module identity, native driver, transport and illumination limits | +| Touch/pen | Native transport and userspace exist; fresh-boot autoload repair is a separate series | Preserve repair and investigate actual lifecycle gaps | +| Suspend | Several device and SoC resource owners are involved | Decode power policies and compare failed Linux transitions with measured Windows behaviour | +| Sensors | Surface SAM support is distinct from the Qualcomm sensor hub | Transport, exact board configuration, orientation and calibration | +| USB4 | Router/retimer groundwork exists | Resource ownership, sideband and working firmware/ring lifecycle | +| Audio | Working protected kernel and userspace path; an ALSA update replaced the selector | Preserve sound while completing persistent userspace selection and scoped compatibility | + +Hardware identity comes from the actual board, firmware tables and device +identification. The bundle includes optional docks, modems, SSDs, panels and +camera assemblies; membership in the package does not justify enabling them. + +The research checkout began at the older integration revision +`2cbd1ec3e2da385e7bd91fd65c63ba5a8fb5b865`. These tools are independent of that +kernel revision and are fork research infrastructure. Hardware changes must +use an explicitly selected carrier: the tested v8 source is +`8ba2b75713feb5c72bbc11dd8cce4048e23cdeea`, with the subsequent touch autoload +repair tracked in fork PR #69. Upstream `jg/ubuntu-qcom-x1e-7.2.y` had advanced +to 7.2.9 at `d84d9b29dffe2199bf295824f82099ee58997ce7` when inspected on +15 October 2026. That newer integration requires separate qualification. + +## Source and evidence + +Tests use synthetic fixtures and run without the driver package. The source is +GPL-2.0-only. No Windows executable, firmware payload, recovered pseudocode or +private device capture is included. Keep locally generated evidence separate +from the original source changes and preserve the input hashes for every +behavioural finding. Analysis tools are not themselves a working camera or a +proof of full Linux hardware parity. diff --git a/tools/sp11-driver-audit/aeob.mjs b/tools/sp11-driver-audit/aeob.mjs new file mode 100644 index 00000000000000..f7d5e79e09adea --- /dev/null +++ b/tools/sp11-driver-audit/aeob.mjs @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Original structural reader for the observed AeoB variant. No driver execution. +import { createHash } from 'node:crypto'; +import { open } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const AEOB_LIMITS = Object.freeze({ + maxBytes: 16 * 1024 * 1024, + maxNodes: 100_000, + maxDepth: 32, +}); + +export class AeobDecodeError extends Error { + constructor(code, message, offset = 0) { + super(message); + this.name = 'AeobDecodeError'; + this.code = code; + this.offset = offset; + } +} + +const digest = (bytes) => createHash('sha256').update(bytes).digest('hex'); + +function limitsFor(options) { + const limits = { ...AEOB_LIMITS }; + for (const [key, value] of Object.entries(options)) { + if (!Object.hasOwn(AEOB_LIMITS, key)) throw new TypeError(`Unknown decoder option: ${key}`); + if (!Number.isSafeInteger(value) || value < 1 || value > AEOB_LIMITS[key]) { + throw new RangeError(`${key} must be an integer from 1 to ${AEOB_LIMITS[key]}`); + } + limits[key] = value; + } + return limits; +} + +/** + * Decode structure only, using observations from this Surface driver bundle. + * Header count is checked against parsed top-level records. Unknown tags are + * opaque and use the observed TLV length convention; incompatible variants + * fail rather than falling back to guessed padding or alternate counts. + * Integer values do not imply GPIOs, physical addresses, rates or policy. + */ +export function decodeAeob(input, options = {}) { + if (!(input instanceof Uint8Array)) throw new TypeError('Input must be bytes'); + const limits = limitsFor(options); + if (input.byteLength > limits.maxBytes) { + throw new AeobDecodeError('BYTE_LIMIT', 'Input exceeds the decoder byte limit'); + } + // Work on a stable bounded copy even when the caller supplied mutable bytes. + const bytes = Buffer.from(input); + const fail = (code, message, offset) => { + throw new AeobDecodeError(code, message, offset); + }; + if (bytes.length < 12) fail('SHORT_HEADER', 'AeoB header requires 12 bytes', 0); + if (!bytes.subarray(0, 4).equals(Buffer.from('AeoB', 'ascii'))) { + fail('BAD_MAGIC', 'Input does not have AeoB magic', 0); + } + const declaredBytes = bytes.readUInt32LE(4); + const declaredTopLevelCount = bytes.readUInt32LE(8); + if (declaredBytes !== bytes.length) { + fail('LENGTH_MISMATCH', 'Declared file length differs from actual length', 4); + } + if (declaredTopLevelCount > limits.maxNodes) { + fail('NODE_LIMIT', 'Declared top-level count exceeds the node limit', 8); + } + let nodeCount = 0; + let maxDepthSeen = 0; + const unknownKinds = new Set(); + + function records(start, end, depth) { + if (depth > limits.maxDepth) { + fail('DEPTH_LIMIT', 'Nested records exceed the depth limit', start); + } + maxDepthSeen = Math.max(maxDepthSeen, depth); + const result = []; + let offset = start; + while (offset < end) { + if (end - offset < 4) { + fail('SHORT_RECORD_HEADER', 'Incomplete TLV header in its container', offset); + } + if (++nodeCount > limits.maxNodes) { + fail('NODE_LIMIT', 'Parsed records exceed the node limit', offset); + } + const kind = bytes.readUInt16LE(offset); + const payloadBytes = bytes.readUInt16LE(offset + 2); + const payloadOffset = offset + 4; + const payloadEnd = payloadOffset + payloadBytes; + if (payloadEnd > end) { + fail('RECORD_OVERRUN', 'TLV payload extends past its container', offset); + } + const node = { offset, kind, payloadOffset, payloadBytes }; + const payload = bytes.subarray(payloadOffset, payloadEnd); + if (kind === 0) { + if (payloadBytes !== 8) { + fail('INTEGER_LENGTH', 'Observed integer kind requires an 8-byte payload', offset); + } + node.encoding = 'uint64-le'; + node.value = payload.readBigUInt64LE().toString(10); + } else if (kind === 1) { + if (payloadBytes === 0 || payload[payloadBytes - 1] !== 0) { + fail('STRING_TERMINATOR', 'Observed string kind requires a terminal NUL', offset); + } + for (let i = 0; i < payloadBytes - 1; i++) { + if (payload[i] === 0 || payload[i] > 0x7f) { + fail('STRING_ENCODING', 'String is not single-NUL-terminated ASCII', payloadOffset + i); + } + } + node.encoding = 'ascii-nul'; + node.value = payload.subarray(0, -1).toString('ascii'); + } else if (kind === 3) { + node.encoding = 'nested-tlv'; + node.children = records(payloadOffset, payloadEnd, depth + 1); + } else { + node.encoding = 'opaque'; + node.sha256 = digest(payload); + unknownKinds.add(kind); + } + result.push(node); + offset = payloadEnd; + } + return result; + } + + const decoded = records(12, bytes.length, 0); + if (decoded.length !== declaredTopLevelCount) { + fail('COUNT_MISMATCH', 'Header count differs from parsed top-level record count', 8); + } + return { + schema: 'sp11-aeob-observed-v1', + interpretation: 'structure only; field meanings and other AeoB variants are unverified', + sha256: digest(bytes), + bytes: bytes.length, + header: { magic: 'AeoB', declaredBytes, declaredTopLevelCount }, + stats: { + nodeCount, + maxDepth: maxDepthSeen, + unknownKinds: [...unknownKinds].sort((a, b) => a - b), + }, + records: decoded, + }; +} + +export async function readAeobBytes(path) { + const handle = await open(path, 'r'); + try { + const stat = await handle.stat(); + if (!stat.isFile()) throw new TypeError('Input must be a regular file'); + if (stat.size > AEOB_LIMITS.maxBytes) { + throw new AeobDecodeError('BYTE_LIMIT', 'Input exceeds the decoder byte limit'); + } + // One extra byte detects a file that grew after stat without unbounded read. + const bytes = Buffer.alloc(stat.size + 1); + let offset = 0; + while (offset < bytes.length) { + const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, null); + if (bytesRead === 0) break; + offset += bytesRead; + } + if (offset !== stat.size) { + throw new AeobDecodeError('FILE_CHANGED', 'Input length changed while reading'); + } + return bytes.subarray(0, offset); + } finally { + await handle.close(); + } +} + +async function main(args) { + if (args.length !== 2 || args[0] !== '--input' || !args[1]) { + throw new TypeError('Usage: node tools/sp11-driver-audit/aeob.mjs --input FILE'); + } + const decoded = decodeAeob(await readAeobBytes(args[1])); + process.stdout.write(`${JSON.stringify(decoded, null, 2)}\n`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + main(process.argv.slice(2)).catch((error) => { + process.stderr.write(`${JSON.stringify({ + error: error.code ?? error.name, + message: error.message, + ...(error instanceof AeobDecodeError ? { offset: error.offset } : {}), + })}\n`); + process.exitCode = 1; + }); +} diff --git a/tools/sp11-driver-audit/aeob.test.mjs b/tools/sp11-driver-audit/aeob.test.mjs new file mode 100644 index 00000000000000..e3be1f575be54d --- /dev/null +++ b/tools/sp11-driver-audit/aeob.test.mjs @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-only +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { AEOB_LIMITS, AeobDecodeError, decodeAeob } from './aeob.mjs'; + +// Synthetic fixtures are independently generated, not copied driver payloads. +function tlv(kind, payload) { + const header = Buffer.alloc(4); + header.writeUInt16LE(kind, 0); + header.writeUInt16LE(payload.length, 2); + return Buffer.concat([header, payload]); +} + +function integer(value) { + const payload = Buffer.alloc(8); + payload.writeBigUInt64LE(value); + return tlv(0, payload); +} + +function container(children) { return tlv(3, Buffer.concat(children)); } + +function file(records, count = records.length) { + const body = Buffer.concat(records); + const header = Buffer.alloc(12); + header.write('AeoB', 0, 'ascii'); + header.writeUInt32LE(body.length + 12, 4); + header.writeUInt32LE(count, 8); + return Buffer.concat([header, body]); +} + +function rejects(code, bytes, options) { + assert.throws(() => decodeAeob(bytes, options), (error) => { + assert.ok(error instanceof AeobDecodeError); + assert.equal(error.code, code); + assert.ok(Number.isInteger(error.offset)); + return true; + }); +} + +test('nested structure preserves precise unsigned 64-bit values and offsets', () => { + const bytes = file([container([ + integer(18_446_744_073_709_551_615n), + tlv(1, Buffer.from('reference\0', 'ascii')), + ])]); + const result = decodeAeob(bytes); + assert.equal(result.header.declaredTopLevelCount, 1); + assert.deepEqual(result.stats, { nodeCount: 3, maxDepth: 1, unknownKinds: [] }); + assert.equal(result.records[0].offset, 12); + assert.equal(result.records[0].children[0].payloadOffset, 20); + assert.equal(result.records[0].children[0].value, '18446744073709551615'); + assert.equal(result.records[0].children[1].value, 'reference'); + assert.equal(result.sha256, createHash('sha256').update(bytes).digest('hex')); +}); + +test('empty structure and empty terminated string are valid', () => { + assert.deepEqual(decodeAeob(file([])).records, []); + assert.equal(decodeAeob(file([tlv(1, Buffer.from([0]))])).records[0].value, ''); + assert.deepEqual(decodeAeob(file([container([])])).records[0].children, []); +}); + +test('unknown kind remains opaque with length, offsets and digest only', () => { + const payload = Buffer.from([0xff, 0, 7, 4]); + const result = decodeAeob(file([tlv(17, payload)])); + assert.deepEqual(result.stats.unknownKinds, [17]); + assert.deepEqual(result.records[0], { + offset: 12, kind: 17, payloadOffset: 16, payloadBytes: 4, + encoding: 'opaque', sha256: createHash('sha256').update(payload).digest('hex'), + }); + assert.equal('value' in result.records[0], false); +}); + +test('header, byte limits and declared counts are enforced', () => { + rejects('SHORT_HEADER', Buffer.alloc(11)); + const magic = file([]); magic[0] = 0; + rejects('BAD_MAGIC', magic); + const length = file([]); length.writeUInt32LE(13, 4); + rejects('LENGTH_MISMATCH', length); + rejects('COUNT_MISMATCH', file([integer(1n)], 0)); + rejects('COUNT_MISMATCH', file([container([integer(1n)])], 2)); + rejects('NODE_LIMIT', file([], AEOB_LIMITS.maxNodes + 1)); + rejects('BYTE_LIMIT', file([integer(1n)]), { maxBytes: 20 }); +}); + +test('container length cannot consume siblings or partial headers', () => { + rejects('SHORT_RECORD_HEADER', file([Buffer.from([0, 0, 8])])); + const overrun = Buffer.from([0, 0, 8, 0, 1]); + rejects('RECORD_OVERRUN', file([overrun])); + rejects('SHORT_RECORD_HEADER', file([container([Buffer.from([0, 0])])])); + rejects('RECORD_OVERRUN', file([container([overrun]), integer(1n)])); + // Extra bytes after an opaque record are not silently treated as padding. + rejects('SHORT_RECORD_HEADER', file([container([ + tlv(2, Buffer.from([0x11, 0x22])), Buffer.from([0, 0]), + ])])); +}); + +test('observed integer and string encodings reject unsupported forms', () => { + rejects('INTEGER_LENGTH', file([tlv(0, Buffer.alloc(7))])); + rejects('STRING_TERMINATOR', file([tlv(1, Buffer.alloc(0))])); + rejects('STRING_TERMINATOR', file([tlv(1, Buffer.from('no nul'))])); + rejects('STRING_ENCODING', file([tlv(1, Buffer.from([65, 0, 66, 0]))])); + rejects('STRING_ENCODING', file([tlv(1, Buffer.from([0xc3, 0xa9, 0]))])); +}); + +test('recursive depth and total record counts are independently bounded', () => { + const nested = file([container([container([integer(1n)])])]); + rejects('DEPTH_LIMIT', nested, { maxDepth: 1 }); + rejects('NODE_LIMIT', nested, { maxNodes: 2 }); + assert.equal(decodeAeob(nested, { maxDepth: 2, maxNodes: 3 }).stats.nodeCount, 3); + for (const options of [{ maxBytes: 0 }, { maxNodes: 1.2 }, { maxDepth: 33 }]) { + assert.throws(() => decodeAeob(file([]), options), RangeError); + } + assert.throws(() => decodeAeob(file([]), { guessedPadding: true }), TypeError); + assert.throws(() => decodeAeob(file([]), { toString: 1 }), TypeError); + assert.throws(() => decodeAeob('AeoB'), TypeError); +}); + +test('CLI emits JSON on success and a bounded diagnostic on failure', async () => { + const temporary = await mkdtemp(join(tmpdir(), 'sp11-aeob-test-')); + try { + const input = join(temporary, 'synthetic.bin'); + await writeFile(input, file([integer(42n)])); + const cli = fileURLToPath(new URL('./aeob.mjs', import.meta.url)); + const ok = spawnSync(process.execPath, [cli, '--input', input], { encoding: 'utf8' }); + assert.equal(ok.status, 0, ok.stderr); + assert.equal(JSON.parse(ok.stdout).records[0].value, '42'); + assert.equal(ok.stderr, ''); + await writeFile(input, Buffer.alloc(4)); + const bad = spawnSync(process.execPath, [cli, '--input', input], { encoding: 'utf8' }); + assert.equal(bad.status, 1); + assert.equal(bad.stdout, ''); + assert.equal(JSON.parse(bad.stderr).error, 'SHORT_HEADER'); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); diff --git a/tools/sp11-driver-audit/camera-config.mjs b/tools/sp11-driver-audit/camera-config.mjs new file mode 100644 index 00000000000000..814391ee39942b --- /dev/null +++ b/tools/sp11-driver-audit/camera-config.mjs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Original metadata interpretation from the recorded field specification. +// These are Windows resource indices, not Linux GPIOs or device-tree values. +import {decodeAeob,readAeobBytes} from './aeob.mjs'; +import {fileURLToPath} from 'node:url'; +import {resolve} from 'node:path'; + +function words(input) { + const decoded=decodeAeob(input); + if(decoded.records.length!==1||decoded.records[0].kind!==3)throw new Error('Expected one observed camera tuple'); + const children=decoded.records[0].children; + if(children.some(x=>x.kind!==0||BigInt(x.value)>0xffffffffn))throw new Error('Expected 32-bit camera fields in integer records'); + return {sha256:decoded.sha256,words:children.map(x=>({value:Number(x.value),offset:x.payloadOffset}))}; +} + +export function decodePrivacyConfiguration(input) { + const source=words(input); + if(source.words.length!==6)throw new Error('Observed privacy configuration requires six descriptors'); + const descriptors=source.words.map(({value,offset},index)=>{ + const mechanismCode=(value>>>8)&3; + const record={index,offset,packed:`0x${value.toString(16).padStart(8,'0')}`,sensorSlots:Array.from({length:8},(_,i)=>i).filter(i=>(value&(1<>>10)&7; + Object.assign(record,{mechanism:'gpio',resourceIndex,activeLow:Boolean(value&(1<<13)),executionPath:resourceIndex<6?'resource-hub-write':'log-only-selector'}); + } + else if(mechanismCode===3)Object.assign(record,{mechanism:'component-pstate',componentIndex:(value>>>10)&7}); + else if(mechanismCode===2)Object.assign(record,{mechanism:'pmic',opaqueSelectors:{low:(value>>>10)&7,high:(value>>>13)&7,resource:(value>>>16)&0x3ff}}); + else record.mechanism='inactive-or-unsupported'; + return record; + }); + return {schema:'sp11-camera-prld-observed-v1',sourceSHA256:source.sha256,descriptors,limits:['Sensor slots require independent identity mapping.','Resource-hub indices are not physical pin numbers.','Polarity describes a Windows GPIO operation; no Linux GPIO mapping is generated.','No hardware accessed or configured.']}; +} + +export function decodePlatformConfiguration(input) { + const source=words(input); + if(source.words.length<9)throw new Error('Observed platform configuration requires flags and eight sensor words'); + const flags=source.words[0].value; + const sensorWords=source.words.slice(1,9).map(({value,offset},index)=>({ + index,bank:Math.floor(index/4),slot:index%4,offset,packed:`0x${value.toString(16).padStart(8,'0')}`, + orientationCode:value&15,directionCode:(value>>>4)&15, + flashPresent:Boolean(value&(1<<8)),flashIndex:(value>>>9)&7, + flashCciTimerIndex:(value>>>12)&3,triggerType:(value>>>14)&3, + windowsI2CMaster:(value>>>16)&15,csiPhyIndex:(value>>>20)&15, + faceAuthentication:Boolean(value&(1<<24)),shutterType:(value>>>25)&1, + })); + return {schema:'sp11-camera-pcfg-observed-v1',sourceSHA256:source.sha256,flags:{packed:`0x${flags.toString(16)}`,sensorSlotMask:flags&15,uninterpretedBits4To7:(flags>>>4)&15,hasPositionOffsets:Boolean(flags&(1<<8)),hasPrivacyShutter:Boolean(flags&(1<<9))},sensorWords,uninterpretedTail:source.words.slice(9),limits:['Four logical slots share one presence mask; two alternative word banks are preserved.','Choosing a bank requires independently verified platform selection.','Orientation codes are not converted to degrees.','Windows I2C master indices are not automatically Linux CCI bus numbers.','No device-tree or runtime configuration is generated.']}; +} + +if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url)) { + const args=process.argv.slice(2); + try { + if(args.length!==4||args[0]!=='--kind'||!['prld','pcfg'].includes(args[1])||args[2]!=='--input')throw new Error('Usage: node camera-config.mjs --kind prld|pcfg --input FILE'); + const input=await readAeobBytes(args[3]); + console.log(JSON.stringify((args[1]==='prld'?decodePrivacyConfiguration:decodePlatformConfiguration)(input),null,2)); + }catch(error){console.error(JSON.stringify({error:error.code??error.name,message:error.message}));process.exitCode=1;} +} diff --git a/tools/sp11-driver-audit/camera-config.test.mjs b/tools/sp11-driver-audit/camera-config.test.mjs new file mode 100644 index 00000000000000..dca4bad281c495 --- /dev/null +++ b/tools/sp11-driver-audit/camera-config.test.mjs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-only +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {decodePrivacyConfiguration,decodePlatformConfiguration} from './camera-config.mjs'; + +function fixture(values) { + const records=values.map(v=>{const bytes=Buffer.alloc(12);bytes.writeUInt16LE(0);bytes.writeUInt16LE(8,2);bytes.writeBigUInt64LE(BigInt(v),4);return bytes;}); + const header=Buffer.alloc(16);header.write('AeoB');header.writeUInt32LE(16+12*values.length,4);header.writeUInt32LE(1,8);header.writeUInt16LE(3,12);header.writeUInt16LE(12*values.length,14); + return Buffer.concat([header,...records]); +} + +test('privacy descriptors separate sensor membership, mechanism and resource index',()=>{ + const decoded=decodePrivacyConfiguration(fixture([0x106,0x501,0,0,0,0])); + assert.deepEqual(decoded.descriptors[0].sensorSlots,[1,2]); + assert.equal(decoded.descriptors[0].resourceIndex,0); + assert.equal(decoded.descriptors[1].resourceIndex,1); + assert.equal(decoded.descriptors[1].mechanism,'gpio'); + assert.equal(decoded.descriptors[1].executionPath,'resource-hub-write'); + assert.equal('physicalGPIO' in decoded.descriptors[1],false); +}); +test('privacy polarity and component selection are distinct fields',()=>{ + const result=decodePrivacyConfiguration(fixture([(1<<13)|(5<<10)|(1<<8)|1,(4<<10)|(3<<8)|2,0,0,0,0])); + assert.equal(result.descriptors[0].activeLow,true);assert.equal(result.descriptors[0].resourceIndex,5); + assert.equal(result.descriptors[1].componentIndex,4);assert.equal('activeLow' in result.descriptors[1],false); +}); +test('platform fields preserve both banks without inventing camera identity',()=>{ + const decoded=decodePlatformConfiguration(fixture([0x107,0x110000,0x230010,0x1000110,0,0,0,0,0,123])); + assert.equal(decoded.flags.sensorSlotMask,7);assert.equal(decoded.flags.hasPositionOffsets,true); + assert.equal(decoded.sensorWords[0].windowsI2CMaster,1);assert.equal(decoded.sensorWords[0].csiPhyIndex,1); + assert.equal(decoded.sensorWords[1].windowsI2CMaster,3);assert.equal(decoded.sensorWords[1].csiPhyIndex,2); + assert.equal(decoded.sensorWords[2].faceAuthentication,true);assert.equal(decoded.sensorWords[4].bank,1); + assert.equal('sensorName' in decoded.sensorWords[0],false);assert.equal(decoded.uninterpretedTail[0].value,123); +}); +test('unsupported tuple sizes and large fields cannot be silently truncated',()=>{ + assert.throws(()=>decodePrivacyConfiguration(fixture([1])),/six/); + assert.throws(()=>decodePlatformConfiguration(fixture([1])),/eight/); + assert.throws(()=>decodePrivacyConfiguration(fixture([0x100000000n,0,0,0,0,0])),/32-bit/); +}); +test('platform flags describe four logical slots and retain other bits',()=>{ + const decoded=decodePlatformConfiguration(fixture([0x1f7,0,0,0,0,0,0,0,0])); + assert.equal(decoded.flags.sensorSlotMask,7); + assert.equal(decoded.flags.uninterpretedBits4To7,15); +}); +test('GPIO selectors six and seven are logged rather than written',()=>{ + const decoded=decodePrivacyConfiguration(fixture([(6<<10)|0x101,(7<<10)|0x101,0,0,0,0])); + assert.equal(decoded.descriptors[0].resourceIndex,6); + assert.equal(decoded.descriptors[1].resourceIndex,7); + assert.equal(decoded.descriptors[0].executionPath,'log-only-selector'); + assert.equal(decoded.descriptors[1].executionPath,'log-only-selector'); +}); +test('slot bit four is opaque and timer bits remain separate from trigger type',()=>{ + const decoded=decodePlatformConfiguration(fixture([0x10,(2<<12)|(3<<14),0,0,0,0,0,0,0])); + assert.equal(decoded.flags.sensorSlotMask,0); + assert.equal(decoded.flags.uninterpretedBits4To7,1); + assert.equal(decoded.sensorWords[0].flashCciTimerIndex,2); + assert.equal(decoded.sensorWords[0].triggerType,3); +}); diff --git a/tools/sp11-driver-audit/camera-fields.md b/tools/sp11-driver-audit/camera-fields.md new file mode 100644 index 00000000000000..31c01463a338f1 --- /dev/null +++ b/tools/sp11-driver-audit/camera-fields.md @@ -0,0 +1,76 @@ +# Camera field observations + +These field definitions come from static analysis of the shipped ARM64 +`qccamplatform8380.sys`, SHA-256 +`836714ec41f92f45af363d7bf3c9b9cfc2cccdbd19a1c57f355b471068648049`. +REA 6.5.0 with Ghidra 12.1.4 imported the PE image at `0x140000000`. +Function boundaries, string references, recovered control flow and ARM64 +instructions were inspected locally. The original inputs and recovered code +remain local; this document records interoperable data-field observations. + +## PRLD + +The routine at `0x140001858`, referenced by the privacy-configuration log +string, consumes six numeric fields from the first nested tuple. Its ARM64 +instructions at `0x140001a18` through `0x140001ad4` confirm the bit extraction. +The execution path at `0x140001b38` distinguishes GPIO, PMIC and component +P-state operations and maintains aggregate usage counters. + +| Bits | Observed role | Limits | +| --- | --- | --- | +| 0–7 | Membership in eight sensor slots | Mapping slots to physical sensors requires separate evidence | +| 8–9 | Mechanism: 1 GPIO, 2 PMIC, 3 component P-state | Zero is inactive or unsupported | +| 10–12 | GPIO selector or component index | GPIO selectors 0–5 use Resource Hub; 6–7 only log in the observed path | +| 13 | GPIO output inversion | Applies to the GPIO mechanism | +| 10–15, 16–25 | PMIC selector fields | Their physical meanings are not established | + +For the observed MSHW0495 file, `0x106` selects slots 1 and 2 through GPIO +resource index 0; `0x501` selects slot 0 through GPIO resource index 1. Thus the +decimal value 1281 is a packed descriptor. Neither value is a physical GPIO. +The mapping from these resource indices to the board's GPIO resources remains +separate from this format decoding. + +## PCFG + +The routine at `0x140002ef0` reads a flag field and eight sensor words, then +handles four logical sensor slots. Flag bits 0–3 form their presence mask; +bits 4–7 remain uninterpreted. It selects the second group of four words when +another callback returns `0x27b`; the interpreter preserves both banks. This +observation does not identify the callback's platform ID namespace. + +The ARM64 bit extractions at `0x140003238` through `0x1400032a4` and the adjacent +field-specific log arguments support these names: + +| Bits in each sensor word | Observed field | +| --- | --- | +| 0–3 | Orientation enumeration | +| 4–7 | Direction enumeration | +| 8 | Flash presence | +| 9–11 | Flash index | +| 12–13 | Flash/CCI timer index | +| 14–15 | Trigger type | +| 16–19 | Windows I2C master index | +| 20–23 | CSIPHY index | +| 24 | Face-authentication flag | +| 25 | Shutter type | + +Flag bit 8 indicates position-offset data and bit 9 indicates privacy-shutter +data. Tail words remain uninterpreted in the source tool. Orientation values +are not degrees, Windows I2C indices are not a proven Linux bus mapping, and +neither CSI lane layout nor link frequency follows from these fields. + +The MSHW0495 bank-zero words begin `0x00110000`, `0x00230010`, `0x01000110`. +The second word's CSIPHY 2 agrees with the existing front-camera graph. Its +Windows master 3 still needs an independent mapping to Linux `cci1_i2c1`. +The PHY match is corroboration; it does not alone prove the identities of every +sensor slot or justify enabling the rear or IR hardware. + +## Linux boundary + +The production driver must use native Linux firmware-node properties and +driver frameworks. It must not load these Windows configuration containers or +require the MSI at build or runtime. Recovered resource requirements inform +reviewed native configuration; unresolved physical mappings remain explicit +research tasks. Sensor identification, power ownership, error unwinding, +stream lifetime and indicator sharing require their own implementation and +tests. diff --git a/tools/sp11-driver-audit/inventory.mjs b/tools/sp11-driver-audit/inventory.mjs new file mode 100644 index 00000000000000..1cf806f71dd797 --- /dev/null +++ b/tools/sp11-driver-audit/inventory.mjs @@ -0,0 +1,129 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: GPL-2.0-only +// Independently written static metadata inventory; never executes an input. +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import {fileURLToPath} from 'node:url'; + +export function splitFields(text) { + const fields=[];let field='',quoted=false; + for(let i=0;i16*1024*1024)throw new Error('INF exceeds size bound'); + const {text,encoding}=decodeInf(bytes),sections=new Map();let section=null,continued=''; + const lines=text.split(/\r?\n/); + if(lines.length>200000)throw new Error('INF exceeds line bound'); + for(let i=0;i{ + let current=value; + for(let i=0;i<8;i++){const next=current.replace(/%([^%]+)%/g,(whole,key)=>strings.get(key.toLowerCase())??whole);if(next===current)return next;current=next;} + return current; + }; + const version=Object.fromEntries((sections.get('version')||[]).filter(x=>x.key!==null).map(x=>{const values=splitFields(x.value).map(expand);return [x.key.toLowerCase(),values.length===1?values[0]:values];})); + const declared=new Set(),missing=[]; + for(const row of sections.get('manufacturer')||[]) { + const [base,...decorations]=splitFields(expand(row.value)); + for(const name of decorations.length?decorations.map(d=>`${base}.${d}`):[base]) { + const key=name.toLowerCase();if(sections.has(key))declared.add(key);else missing.push(name); + } + } + const models=[]; + for(const name of declared)for(const row of sections.get(name)) { + if(row.key===null)continue; + const [installSection,...identities]=splitFields(expand(row.value)); + // INF model positions define identities; GUID and software enumerators are + // valid too. A guessed bus-name grammar would silently lose whole drivers. + const [hardwareID,...compatibleIDs]=identities; + if(hardwareID)models.push({section:name,line:row.line,description:expand(row.key).replace(/^"|"$/g,''),installSection,hardwareID,compatibleIDs:compatibleIDs.filter(x=>x.length>0)}); + } + const services=[],serviceBinaries=[],sourceFiles=[]; + for(const [name,rows] of sections)for(const row of rows) { + if(row.key?.toLowerCase()==='addservice')services.push({section:name,line:row.line,fields:splitFields(expand(row.value))}); + if(row.key?.toLowerCase()==='servicebinary')serviceBinaries.push({section:name,line:row.line,value:expand(splitFields(row.value)[0])}); + if(/^sourcedisksfiles(?:\.|$)/.test(name)&&row.key!==null)sourceFiles.push({section:name,line:row.line,name:expand(row.key)}); + } + return {encoding,version,models,missingDeclaredModelSections:missing,services,serviceBinaries,sourceFiles,warnings,limits:['Declared package bindings only; not evidence of installed hardware.','AddReg and executable driver semantics are not inferred.','Malformed string declarations are reported and left unresolved.']}; +} + +export function inspectPEHeader(bytes) { + if(bytes.length<64||bytes.toString('ascii',0,2)!=='MZ')return null; + const offset=bytes.readUInt32LE(0x3c); + if(offset>bytes.length-24||bytes.toString('ascii',offset,offset+4)!=='PE\0\0')return {format:'MZ',status:'PE header unavailable or invalid'}; + const machine=bytes.readUInt16LE(offset+4),names={0xaa64:'ARM64',0x8664:'AMD64',0x14c:'I386',0xa641:'ARM64EC',0xa64e:'ARM64X'}; + return {format:'PE',machine:`0x${machine.toString(16)}`,architecture:names[machine]??'unknown',sections:bytes.readUInt16LE(offset+6)}; +} + +async function digest(file) { + const hash=crypto.createHash('sha256');for await(const chunk of fs.createReadStream(file))hash.update(chunk);return hash.digest('hex'); +} + +export async function inventory(input) { + const root=fs.realpathSync(input);if(!fs.statSync(root).isDirectory())throw new Error('Input must be an extracted directory'); + const files=[],skipped=[]; + async function walk(dir) { + const entries=fs.readdirSync(dir,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name,'en')); + for(const entry of entries) { + const absolute=path.join(dir,entry.name),relative=path.relative(root,absolute).split(path.sep).join('/'); + if(entry.isSymbolicLink()){skipped.push({path:relative,reason:'symbolic link'});continue;} + if(entry.isDirectory()){await walk(absolute);continue;} + if(!entry.isFile()){skipped.push({path:relative,reason:'not a regular file'});continue;} + const stat=fs.statSync(absolute),record={path:relative,size:stat.size,sha256:await digest(absolute)}; + if(/\.inf$/i.test(entry.name)){try{record.inf=parseInf(fs.readFileSync(absolute));}catch(e){record.infError=e.message;}} + const handle=fs.openSync(absolute,'r');try{const header=Buffer.alloc(Math.min(stat.size,65536));fs.readSync(handle,header,0,header.length,0);const pe=inspectPEHeader(header);if(pe)record.native=pe;if(header.toString('ascii',0,4)==='AeoB')record.container='AeoB';}finally{fs.closeSync(handle);} + files.push(record); + } + } + await walk(root); + const byExtension={},byArchitecture={};for(const file of files){const ext=path.extname(file.path).toLowerCase()||'(none)';byExtension[ext]=(byExtension[ext]||0)+1;if(file.native?.architecture)byArchitecture[file.native.architecture]=(byArchitecture[file.native.architecture]||0)+1;} + return {schemaVersion:1,inputLabel:path.basename(root),summary:{files:files.length,bytes:files.reduce((n,f)=>n+f.size,0),infFiles:files.filter(f=>f.inf).length,infErrors:files.filter(f=>f.infError).length,aeobFiles:files.filter(f=>f.container==='AeoB').length,byExtension,byArchitecture},files,skipped,interpretation:'Static package inventory. No input executed, no files installed, and no hardware support claim.'}; +} + +if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)) { + const args=process.argv.slice(2); + if(args.length!==1){console.error('Usage: node inventory.mjs /path/to/extracted/SurfaceUpdate > inventory.json');process.exitCode=64;} + else try{console.log(JSON.stringify(await inventory(args[0]),null,2));}catch(e){console.error(e.message);process.exitCode=1;} +} diff --git a/tools/sp11-driver-audit/inventory.test.mjs b/tools/sp11-driver-audit/inventory.test.mjs new file mode 100644 index 00000000000000..9d0d11a4252e08 --- /dev/null +++ b/tools/sp11-driver-audit/inventory.test.mjs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-only +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {decodeInf,parseInf,splitFields,inspectPEHeader} from './inventory.mjs'; + +test('quoted commas, escaped quotes and semicolons survive field parsing',()=>{ + assert.deepEqual(splitFields('a,"b,c","d""e",f'),['a','b,c','d"e','f']); + assert.throws(()=>splitFields('"oops'),/Unterminated/); +}); +test('only manufacturer-referenced model sections define device bindings',()=>{ + const inf=Buffer.from('[Version]\nClass=Camera\nProvider=%Vendor%\n[Manufacturer]\n%Vendor%=Models,NTarm64.10.0\n[Models.NTarm64.10.0]\n%Description%=Sensor,ACPI\\SONY0681,ACPI\\SONY0681&SUBSYS_MSHW0490 ;comment\n[Unreferenced]\nignored=Fake,ACPI\\FAKE0001\n[Sensor.Services]\nAddService=Camera,2,Service\n[Service]\nServiceBinary=%12%\\sensor.sys\n[Strings]\nVendor="Microsoft; vendor"\nDescription="Front, camera"\n'); + const parsed=parseInf(inf); + assert.equal(parsed.version.provider,'Microsoft; vendor'); + assert.equal(parsed.models.length,1); + assert.equal(parsed.models[0].description,'Front, camera'); + assert.equal(parsed.models[0].hardwareID,'ACPI\\SONY0681'); + assert.deepEqual(parsed.models[0].compatibleIDs,['ACPI\\SONY0681&SUBSYS_MSHW0490']); + assert.equal(parsed.serviceBinaries[0].value,'%12%\\sensor.sys'); +}); +test('UTF16 and continued INF rows retain the complete device identity',()=>{ + const input=Buffer.concat([Buffer.from([255,254]),Buffer.from('[Manufacturer]\r\nM=Models,NTarm64\r\n[Models.NTarm64]\r\nD=Install,\\\r\n ACPI\\DEVICE01\r\n','utf16le')]); + assert.equal(decodeInf(input).encoding,'UTF-16LE'); + assert.equal(parseInf(input).models[0].hardwareID,'ACPI\\DEVICE01'); + assert.throws(()=>decodeInf(Buffer.from([255,254,65])),/Odd UTF-16LE/); +}); +test('missing decorated sections are explicit instead of assuming support',()=>{ + const parsed=parseInf(Buffer.from('[Manufacturer]\nM=Models,NTarm64\n[Models]\nD=Install,ACPI\\DEVICE01\n')); + assert.equal(parsed.models.length,0);assert.deepEqual(parsed.missingDeclaredModelSections,['Models.NTarm64']); +}); +test('DriverVer preserves both date and package version',()=>{ + assert.deepEqual(parseInf(Buffer.from('[Version]\nDriverVer=04/09/2026,1.2.3.4\n')).version.driverver,['04/09/2026','1.2.3.4']); +}); +test('GUID enumerators and software identities are retained by model position',()=>{ + const parsed=parseInf(Buffer.from('[Manufacturer]\nM=Models\n[Models]\nD=Install,{4047F442-B3B0-4517-8C6F-9EEA770022B1}\\SurfacePowerStateDriver,SWC\\Component\n')); + assert.equal(parsed.models[0].hardwareID,'{4047F442-B3B0-4517-8C6F-9EEA770022B1}\\SurfacePowerStateDriver'); + assert.deepEqual(parsed.models[0].compatibleIDs,['SWC\\Component']); +}); +test('malformed unused strings retain evidence without losing other bindings',()=>{ + const parsed=parseInf(Buffer.from('[Manufacturer]\nM=Models\n[Models]\nD=Install,ACPI\\DEVICE01\n[Strings]\nbad="missing terminator\n')); + assert.equal(parsed.models.length,1);assert.equal(parsed.warnings.length,1); + assert.equal(parsed.warnings[0].line,6); +}); +test('PE architecture comes from bounded headers, not extension or package title',()=>{ + assert.equal(inspectPEHeader(Buffer.from('not a PE')),null); + const bytes=Buffer.alloc(128);bytes.write('MZ');bytes.writeUInt32LE(64,0x3c);bytes.write('PE\0\0',64);bytes.writeUInt16LE(0xaa64,68);bytes.writeUInt16LE(4,70); + assert.equal(inspectPEHeader(bytes).architecture,'ARM64'); + bytes.writeUInt32LE(999999,0x3c);assert.equal(inspectPEHeader(bytes).status,'PE header unavailable or invalid'); +}); From cb52a9c8700fe2bc70a72fc7078d7008d7082d4d Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 11:46:20 +0100 Subject: [PATCH 02/17] tools: decode SP11 sensor metadata and ACPI argument storage Add a bounded, independently written Chromatix container reader with synthetic tests. Resolve named register references for private research while keeping default reports limited to identities, offsets and digests. Recognize the ACPI V1 minimum argument storage and four-byte integers alongside the observed eight-byte form. Record REA-verified sensor probe fields and power-resource delay units without generating hardware configuration or adding Windows dependencies. --- tools/sp11-driver-audit/README.md | 41 ++- tools/sp11-driver-audit/aeob.mjs | 39 +- tools/sp11-driver-audit/aeob.test.mjs | 35 +- tools/sp11-driver-audit/camera-config.mjs | 38 +- .../sp11-driver-audit/camera-config.test.mjs | 52 ++- tools/sp11-driver-audit/camera-fields.md | 63 ++++ tools/sp11-driver-audit/chromatix.mjs | 332 ++++++++++++++++++ tools/sp11-driver-audit/chromatix.test.mjs | 209 +++++++++++ 8 files changed, 778 insertions(+), 31 deletions(-) create mode 100644 tools/sp11-driver-audit/chromatix.mjs create mode 100644 tools/sp11-driver-audit/chromatix.test.mjs diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index cb1b3c82f9bc2c..c11fe250c8a389 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -24,6 +24,10 @@ node tools/sp11-driver-audit/camera-config.mjs \ --kind prld --input /path/to/CAMP_PRLD_MSHW0495.bin node tools/sp11-driver-audit/camera-config.mjs \ --kind pcfg --input /path/to/CAMP_PCFG_MSHW0495.bin +node tools/sp11-driver-audit/camera-config.mjs \ + --kind scfg --input /path/to/SCFG_REAR_MSHW0491.bin +node tools/sp11-driver-audit/chromatix.mjs \ + --input /path/to/com.surface.sensormodule.rfc_ov13858.bin node --test tools/sp11-driver-audit/*.test.mjs ``` @@ -35,17 +39,27 @@ Service declarations include sections that might never be selected. Broken string declarations remain unresolved and produce warnings. Child symbolic links are skipped. INF path strings are never opened as commands or files. -The AeoB reader checks lengths, top-level counts, nested boundaries, integer -precision and resource limits. It recognizes the observed integer, string and -nested tuple forms. Unknown tags retain a digest and byte range without a -guessed interpretation. Unsupported layouts fail explicitly. Generated JSON -is research data and should remain outside the kernel source tree. +The AeoB reader checks the ACPI V1 output-buffer structure: lengths, top-level +counts, nested boundaries and the minimum four-byte argument data union. This +does not align every payload to four bytes. Documented four-byte integers and +observed eight-byte integers retain full precision. Buffers and unknown tags +retain a digest and byte range without guessed device semantics. Unsupported +layouts fail explicitly. Generated JSON is research data and should remain +outside the kernel source tree. The camera interpreter uses the [recorded field specification](camera-fields.md). It preserves both platform configuration banks, raw values and unknown tail fields. It does not select a board, turn orientation codes into angles, or translate Windows resource indices into Linux GPIO or CCI numbers. +The Chromatix reader validates the observed 3.0.1 container, CRC32, section +extents, named descriptors, register-sequence references and resource budgets. +It does not execute operations or emit Linux driver code. Default output +contains identities, offsets, counts and digests, not register values or raw +payloads. Explicit `--include-registers` and `--include-payloads` options are +for local research; keep their output outside the source tree. Unknown +operation codes must not be interpreted as production policy. + ## Initial measured coverage The April 2026 package named @@ -55,10 +69,19 @@ string declarations reported as warnings. Static PE headers identify 190 ARM64 and 17 I386 objects; the package also contains DSP and firmware objects in other formats. -Of 72 files beginning with `AeoB`, 71 follow the observed layout, containing -46,033 records with maximum nesting depth six. `qcadc/ADC1.bin` uses an -unsupported layout and is rejected at offset 253. This is a coverage limit, -not a claim that the vendor file is corrupt or that all AeoB variants match. +All 72 files beginning with `AeoB` follow the checked ACPI argument-storage +layout, containing 47,008 records with maximum nesting depth six. Two short +arguments require the minimum data union; the earlier narrow reader rejected +`qcadc/ADC1.bin` because it missed this documented storage rule. Structural +decoding does not establish the meaning of ADC resources or coverage of every +possible argument encoding. + +The sensor-container reader validates the inspected IMX681, OV13858 and three +OV02C10 containers. The front module has 3,035 named descriptors and 915 register +rows; the rear has 2,117 descriptors and 632 rows. Some TPG and IR records use +unsupported semantics and fail explicitly. Front sequences were compared with +public IMX681 reference tables; rear output sizes agree with output-size +registers. Neither check establishes CSI timing or successful rear capture. ## Implementation tracks diff --git a/tools/sp11-driver-audit/aeob.mjs b/tools/sp11-driver-audit/aeob.mjs index f7d5e79e09adea..185d5f6a51c88d 100644 --- a/tools/sp11-driver-audit/aeob.mjs +++ b/tools/sp11-driver-audit/aeob.mjs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-only -// Original structural reader for the observed AeoB variant. No driver execution. +// Original reader for observed ACPI_EVAL_OUTPUT_BUFFER_V1 files. No execution. +// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/acpiioct/ns-acpiioct-_acpi_method_argument_v1 import { createHash } from 'node:crypto'; import { open } from 'node:fs/promises'; import { resolve } from 'node:path'; @@ -35,10 +36,12 @@ function limitsFor(options) { } /** - * Decode structure only, using observations from this Surface driver bundle. - * Header count is checked against parsed top-level records. Unknown tags are - * opaque and use the observed TLV length convention; incompatible variants - * fail rather than falling back to guessed padding or alternate counts. + * Decode structure only. ACPI_METHOD_ARGUMENT_V1 has a four-byte minimum data + * union, even for shorter strings or buffers; this is not four-byte alignment + * of longer payloads. The bundle also uses eight-byte integer payloads, which + * are preserved in full alongside the documented four-byte ULONG form. + * Header count is checked against parsed top-level records. Unknown tags stay + * opaque; incompatible layouts fail rather than guessing alternate counts. * Integer values do not imply GPIOs, physical addresses, rates or policy. */ export function decodeAeob(input, options = {}) { @@ -86,17 +89,21 @@ export function decodeAeob(input, options = {}) { const payloadBytes = bytes.readUInt16LE(offset + 2); const payloadOffset = offset + 4; const payloadEnd = payloadOffset + payloadBytes; - if (payloadEnd > end) { - fail('RECORD_OVERRUN', 'TLV payload extends past its container', offset); + const recordEnd = payloadOffset + Math.max(4, payloadBytes); + if (recordEnd > end) { + fail('RECORD_OVERRUN', 'Argument storage extends past its container', offset); } const node = { offset, kind, payloadOffset, payloadBytes }; + if (payloadBytes < 4) node.paddingBytes = 4 - payloadBytes; const payload = bytes.subarray(payloadOffset, payloadEnd); if (kind === 0) { - if (payloadBytes !== 8) { - fail('INTEGER_LENGTH', 'Observed integer kind requires an 8-byte payload', offset); + if (payloadBytes !== 4 && payloadBytes !== 8) { + fail('INTEGER_LENGTH', 'Integer requires a 4- or 8-byte payload', offset); } - node.encoding = 'uint64-le'; - node.value = payload.readBigUInt64LE().toString(10); + node.encoding = payloadBytes === 8 ? 'uint64-le' : 'uint32-le'; + node.value = payloadBytes === 8 + ? payload.readBigUInt64LE().toString(10) + : payload.readUInt32LE().toString(10); } else if (kind === 1) { if (payloadBytes === 0 || payload[payloadBytes - 1] !== 0) { fail('STRING_TERMINATOR', 'Observed string kind requires a terminal NUL', offset); @@ -112,12 +119,12 @@ export function decodeAeob(input, options = {}) { node.encoding = 'nested-tlv'; node.children = records(payloadOffset, payloadEnd, depth + 1); } else { - node.encoding = 'opaque'; + node.encoding = kind === 2 ? 'buffer' : 'opaque'; node.sha256 = digest(payload); - unknownKinds.add(kind); + if (kind !== 2) unknownKinds.add(kind); } result.push(node); - offset = payloadEnd; + offset = recordEnd; } return result; } @@ -127,8 +134,8 @@ export function decodeAeob(input, options = {}) { fail('COUNT_MISMATCH', 'Header count differs from parsed top-level record count', 8); } return { - schema: 'sp11-aeob-observed-v1', - interpretation: 'structure only; field meanings and other AeoB variants are unverified', + schema: 'sp11-acpi-output-observed-v2', + interpretation: 'ACPI output structure only; device field meanings remain unverified', sha256: digest(bytes), bytes: bytes.length, header: { magic: 'AeoB', declaredBytes, declaredTopLevelCount }, diff --git a/tools/sp11-driver-audit/aeob.test.mjs b/tools/sp11-driver-audit/aeob.test.mjs index e3be1f575be54d..7dd5680cc4fb0e 100644 --- a/tools/sp11-driver-audit/aeob.test.mjs +++ b/tools/sp11-driver-audit/aeob.test.mjs @@ -10,11 +10,11 @@ import { fileURLToPath } from 'node:url'; import { AEOB_LIMITS, AeobDecodeError, decodeAeob } from './aeob.mjs'; // Synthetic fixtures are independently generated, not copied driver payloads. -function tlv(kind, payload) { +function tlv(kind, payload, paddingValue = 0) { const header = Buffer.alloc(4); header.writeUInt16LE(kind, 0); header.writeUInt16LE(payload.length, 2); - return Buffer.concat([header, payload]); + return Buffer.concat([header, payload, Buffer.alloc(Math.max(0, 4 - payload.length), paddingValue)]); } function integer(value) { @@ -75,6 +75,35 @@ test('unknown kind remains opaque with length, offsets and digest only', () => { assert.equal('value' in result.records[0], false); }); +test('short arguments consume a minimum four-byte data union, not alignment', () => { + const payload = Buffer.from([0x11, 0x22]); + const result = decodeAeob(file([container([ + tlv(2, payload, 0xa5), + tlv(1, Buffer.from('a\0')), + tlv(1, Buffer.from('four\0')), + integer(9n), + ])])); + const children = result.records[0].children; + assert.equal(children[0].encoding, 'buffer'); + assert.equal(children[0].paddingBytes, 2); + assert.equal(children[0].sha256, createHash('sha256').update(payload).digest('hex')); + assert.deepEqual(result.stats.unknownKinds, []); + assert.deepEqual(children.map((item) => item.offset), [16, 24, 32, 41]); + assert.equal(children[3].value, '9'); + // Padding belongs to the data union, but may not run into a sibling. + const truncated = Buffer.from([2, 0, 2, 0, 0x11, 0x22]); + rejects('RECORD_OVERRUN', file([truncated])); + rejects('RECORD_OVERRUN', file([container([truncated]), integer(1n)])); +}); + +test('documented ULONG and observed uint64 integer forms preserve precision', () => { + const payload = Buffer.alloc(4); + payload.writeUInt32LE(0xffff_ffff); + const result = decodeAeob(file([tlv(0, payload)])); + assert.equal(result.records[0].encoding, 'uint32-le'); + assert.equal(result.records[0].value, '4294967295'); +}); + test('header, byte limits and declared counts are enforced', () => { rejects('SHORT_HEADER', Buffer.alloc(11)); const magic = file([]); magic[0] = 0; @@ -93,7 +122,7 @@ test('container length cannot consume siblings or partial headers', () => { rejects('RECORD_OVERRUN', file([overrun])); rejects('SHORT_RECORD_HEADER', file([container([Buffer.from([0, 0])])])); rejects('RECORD_OVERRUN', file([container([overrun]), integer(1n)])); - // Extra bytes after an opaque record are not silently treated as padding. + // Bytes beyond the minimum data union are not silently treated as padding. rejects('SHORT_RECORD_HEADER', file([container([ tlv(2, Buffer.from([0x11, 0x22])), Buffer.from([0, 0]), ])])); diff --git a/tools/sp11-driver-audit/camera-config.mjs b/tools/sp11-driver-audit/camera-config.mjs index 814391ee39942b..d8ef1d82c8c156 100644 --- a/tools/sp11-driver-audit/camera-config.mjs +++ b/tools/sp11-driver-audit/camera-config.mjs @@ -13,6 +13,39 @@ function words(input) { return {sha256:decoded.sha256,words:children.map(x=>({value:Number(x.value),offset:x.payloadOffset}))}; } +export function decodeSensorConfiguration(input) { + const decoded=decodeAeob(input); + if(decoded.records.length!==1||decoded.records[0].kind!==3)throw new Error('Expected one observed sensor tuple'); + const fields=decoded.records[0].children; + if(fields.length!==5||fields[0].kind!==1||fields[1].kind!==1|| + fields.slice(2).some(x=>x.kind!==0||BigInt(x.value)>0xffffffffn)) { + throw new Error('Observed sensor configuration requires two strings and three 32-bit fields'); + } + const [bus,probe,tail]=fields.slice(2).map(x=>Number(x.value)); + const frequencyCode=(bus>>>16)&3; + const registerAddressTypeCode=(bus>>>18)&3; + const registerDataTypeCode=(bus>>>20)&3; + return { + schema:'sp11-camera-scfg-observed-v1',sourceSHA256:decoded.sha256, + names:{sensorModule:fields[0].value,tuning:fields[1].value}, + bus:{offset:fields[2].payloadOffset,packed:`0x${bus.toString(16).padStart(8,'0')}`, + windowsSlaveAddress:bus&0xffff,observedInterfaceAddressByte:bus&0xff,frequencyCode, + observedFrequencySetting:[100,400,1000,400][frequencyCode], + registerAddressTypeCode,registerDataTypeCode, + observedAddressType:[1,2,3,1][registerAddressTypeCode], + observedDataType:[1,2,3,1][registerDataTypeCode], + uninterpretedUpperBits:bus>>>22}, + probe:{offset:fields[3].payloadOffset,register:probe&0xffff,expectedValue:probe>>>16}, + uninterpretedTail:{offset:fields[4].payloadOffset,value:tail}, + limits:['Names are labels only; no referenced file is opened.', + 'The raw Windows slave-address field is not a proven Linux seven-bit address.', + 'Frequency setting units and CCI bus mapping are not established by this consumer.', + 'Enum code three uses a logged Windows fallback; it does not establish hardware capability.', + 'Observed type two uses a two-byte register address and a big-endian two-byte probe value.', + 'These probe fields do not establish power sequencing, mode tables or device-tree wiring.'], + }; +} + export function decodePrivacyConfiguration(input) { const source=words(input); if(source.words.length!==6)throw new Error('Observed privacy configuration requires six descriptors'); @@ -49,8 +82,9 @@ export function decodePlatformConfiguration(input) { if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url)) { const args=process.argv.slice(2); try { - if(args.length!==4||args[0]!=='--kind'||!['prld','pcfg'].includes(args[1])||args[2]!=='--input')throw new Error('Usage: node camera-config.mjs --kind prld|pcfg --input FILE'); + const decoders={prld:decodePrivacyConfiguration,pcfg:decodePlatformConfiguration,scfg:decodeSensorConfiguration}; + if(args.length!==4||args[0]!=='--kind'||!Object.hasOwn(decoders,args[1])||args[2]!=='--input')throw new Error('Usage: node camera-config.mjs --kind prld|pcfg|scfg --input FILE'); const input=await readAeobBytes(args[3]); - console.log(JSON.stringify((args[1]==='prld'?decodePrivacyConfiguration:decodePlatformConfiguration)(input),null,2)); + console.log(JSON.stringify(decoders[args[1]](input),null,2)); }catch(error){console.error(JSON.stringify({error:error.code??error.name,message:error.message}));process.exitCode=1;} } diff --git a/tools/sp11-driver-audit/camera-config.test.mjs b/tools/sp11-driver-audit/camera-config.test.mjs index dca4bad281c495..197e9e3062ac54 100644 --- a/tools/sp11-driver-audit/camera-config.test.mjs +++ b/tools/sp11-driver-audit/camera-config.test.mjs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only import test from 'node:test'; import assert from 'node:assert/strict'; -import {decodePrivacyConfiguration,decodePlatformConfiguration} from './camera-config.mjs'; +import {decodePrivacyConfiguration,decodePlatformConfiguration,decodeSensorConfiguration} from './camera-config.mjs'; function fixture(values) { const records=values.map(v=>{const bytes=Buffer.alloc(12);bytes.writeUInt16LE(0);bytes.writeUInt16LE(8,2);bytes.writeBigUInt64LE(BigInt(v),4);return bytes;}); @@ -55,3 +55,53 @@ test('slot bit four is opaque and timer bits remain separate from trigger type', assert.equal(decoded.sensorWords[0].flashCciTimerIndex,2); assert.equal(decoded.sensorWords[0].triggerType,3); }); + +function sensorFixture(values=[0x00150020,0x12345678,0],names=['sensor-label','tuning-label']) { + const numberRecords=fixture(values).subarray(16); + const strings=names.map(name=>{ + const payload=Buffer.from(`${name}\0`,'ascii'); + const bytes=Buffer.alloc(4+Math.max(4,payload.length)); + bytes.writeUInt16LE(1);bytes.writeUInt16LE(payload.length,2);payload.copy(bytes,4); + return bytes; + }); + const children=Buffer.concat([...strings,numberRecords]); + const header=Buffer.alloc(16);header.write('AeoB'); + header.writeUInt32LE(16+children.length,4);header.writeUInt32LE(1,8); + header.writeUInt16LE(3,12);header.writeUInt16LE(children.length,14); + return Buffer.concat([header,children]); +} + +test('sensor probe fields separate Windows address, enum codes and identity',()=>{ + const decoded=decodeSensorConfiguration(sensorFixture()); + assert.equal(decoded.bus.windowsSlaveAddress,0x20); + assert.equal(decoded.bus.frequencyCode,1); + assert.equal(decoded.bus.observedFrequencySetting,400); + assert.equal(decoded.bus.observedAddressType,2); + assert.equal(decoded.bus.observedDataType,2); + assert.equal(decoded.probe.register,0x5678); + assert.equal(decoded.probe.expectedValue,0x1234); + assert.equal('linuxAddress' in decoded.bus,false); + assert.equal('frequencyHz' in decoded.bus,false); + assert.equal(decoded.names.sensorModule,'sensor-label'); +}); + +test('sensor labels stay inert and opaque fields and enum fallback remain visible',()=>{ + const decoded=decodeSensorConfiguration(sensorFixture([0xfff00023,0xffffffff,7],['../missing','a'])); + assert.equal(decoded.names.sensorModule,'../missing'); + assert.equal(decoded.bus.windowsSlaveAddress,0x23); + assert.equal(decoded.bus.registerDataTypeCode,3); + assert.equal(decoded.bus.observedDataType,1); + assert.equal(decoded.bus.uninterpretedUpperBits,0x3ff); + assert.equal(decoded.uninterpretedTail.value,7); + const wide=decodeSensorConfiguration(sensorFixture([0x00151234,0,0])); + assert.equal(wide.bus.windowsSlaveAddress,0x1234); + assert.equal(wide.bus.observedInterfaceAddressByte,0x34); +}); + +test('sensor interpreter rejects unverified extensions, missing labels and wide fields',()=>{ + assert.throws(()=>decodeSensorConfiguration(fixture([1,2,3])),/two strings/); + assert.throws(()=>decodeSensorConfiguration(sensorFixture([1,2])),/two strings/); + assert.throws(()=>decodeSensorConfiguration(sensorFixture([1,2,3,4])),/two strings/); + assert.throws(()=>decodeSensorConfiguration(sensorFixture([0x100000000n,2,3])),/32-bit/); + assert.throws(()=>decodeSensorConfiguration(sensorFixture([1,2,3],['one'])),/two strings/); +}); diff --git a/tools/sp11-driver-audit/camera-fields.md b/tools/sp11-driver-audit/camera-fields.md index 31c01463a338f1..ba556dd2a4fab7 100644 --- a/tools/sp11-driver-audit/camera-fields.md +++ b/tools/sp11-driver-audit/camera-fields.md @@ -65,6 +65,69 @@ Windows master 3 still needs an independent mapping to Linux `cci1_i2c1`. The PHY match is corroboration; it does not alone prove the identities of every sensor slot or justify enabling the rear or IR hardware. +## SCFG + +The inspected front and rear sensor configuration files each contain two +string labels and three numeric words. REA inspected the front consumer's +getter at `0x140001b48`, probe at `0x140009f18` and read helper at `0x14000ad68`; +the rear equivalents are `0x140005188`, `0x14000d8a0` and `0x14000e6c8`. +The front consumer SHA-256 is +`80a8e4a1ef8f0dacfbc2e8c6919cb269993057ffd3133c2ef7016ff742e46f03`; +the rear consumer SHA-256 is +`b7d7a278c5e7b92ebf35f870a7e06cbad670ffb35bfaf40106e27b09bf33fabb`. + +| First numeric word | Observed role | +| --- | --- | +| Bits 0–15 | Raw Windows slave-address field | +| Bits 16–17 | Frequency code: 0/1/2 map to 100/400/1000; 3 falls back to 400 | +| Bits 18–19 | Register-address type: 0/1/2 map to 1/2/3; 3 falls back to 1 | +| Bits 20–21 | Register-data type: same enumeration mapping | +| Bits 22–31 | Uninterpreted | + +The second word contains the probe register in bits 0–15 and expected value +in bits 16–31. The third word remains opaque. Observed type two uses a two-byte +register address and combines two returned bytes high first. The numerical +frequency-setting units are not established by this consumer. Enum fallback +is observed Windows behavior, not an endorsement of unsupported hardware. + +Rear `0xd855300b` means a probe of register `0x300b` for bytes `d8 55`, agreeing +with the native OV13858 driver's three-byte `00 d8 55` check at `0x300a`. +Front `0x0aff0004` instead describes a probe at `0x0004` for `0aff`; it is not +the existing native IMX681 model-ID check at `0x0016` for `0681` and does not +justify replacing that check. Both inspected files contain raw address `0x20`. +Neither consumer establishes seven-bit normalization or Linux CCI bus mapping. +The 16-bit stored field is copied into a byte in the inspected interface; +both the stored field and that byte are reported without normalizing either. +The tool retains names as inert labels and opens no referenced paths. + +## Power-resource delays + +Static analysis of `qcpep8380.sys`, SHA-256 +`e5bdeb8c0fb8823990f7520a19c00807711cf909f91f8d67db78ffb18c1b2277`, +establishes milliseconds for the normal runtime `DELAY` resource path: + +1. The provider registered at `0x140089880` handles the interface queried by + the camera platform; `0x14003de98` validates the AeoB signature. +2. Type-nine parsing at `0x140414680` copies the first integer unscaled into + resource data at offset `0x18`. +3. Conversion at `0x14040aa74` and construction at `0x1404265f0` preserve it + into runtime-node offset `0x20`. +4. Execution at `0x140128170` converts it to negative `value * 10000` relative + 100-nanosecond ticks for the sleep path, or `value * 1000` microseconds for + the busy-wait path. + +Thus the front and rear resource values 1 and 10 represent 1 ms and 10 ms. +Elapsed time can already satisfy a delay, and the crashdump path only logs its +completion; this is not a claim that every path always sleeps. The platform's +fixed 20-microsecond polling and named sensor-mode delay fields are separate. +These observations do not establish supply mapping, regulator ownership or +safe Linux sequencing by themselves. + +References: [ACPI output-buffer layout](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/acpiioct/ns-acpiioct-_acpi_eval_output_buffer_v1), +[argument storage](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/acpiioct/ns-acpiioct-_acpi_method_argument_v1), +[relative sleep units](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kedelayexecutionthread), +[stall units](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kestallexecutionprocessor). + ## Linux boundary The production driver must use native Linux firmware-node properties and diff --git a/tools/sp11-driver-audit/chromatix.mjs b/tools/sp11-driver-audit/chromatix.mjs new file mode 100644 index 00000000000000..f4cd4ed848a95c --- /dev/null +++ b/tools/sp11-driver-audit/chromatix.mjs @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Independently authored structural reader for observed Chromatix 3.0.1 files. +// No vendor parser, driver code, binary payload or register table is embedded. +import { createHash } from 'node:crypto'; +import { open } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const CHROMATIX_LIMITS = Object.freeze({ + maxBytes: 16 * 1024 * 1024, + maxNodes: 100_000, + maxRegisterRows: 100_000, + maxTotalPayloadBytes: 32 * 1024 * 1024, + maxResolvedValues: 1_000_000, + maxResolvedBytes: 32 * 1024 * 1024, +}); + +export class ChromatixDecodeError extends Error { + constructor(code, message, offset = 0) { + super(message); + this.name = 'ChromatixDecodeError'; + this.code = code; + this.offset = offset; + } +} + +const hash = (bytes) => createHash('sha256').update(bytes).digest('hex'); +const CRC_TABLE = Array.from({ length: 256 }, (_, value) => { + for (let bit = 0; bit < 8; bit++) value = (value >>> 1) ^ ((value & 1) ? 0xedb88320 : 0); + return value >>> 0; +}); + +export function crc32(bytes) { + let value = 0xffffffff; + for (const byte of bytes) value = (value >>> 8) ^ CRC_TABLE[(value ^ byte) & 0xff]; + return (value ^ 0xffffffff) >>> 0; +} + +function optionsFor(options) { + const result = { ...CHROMATIX_LIMITS, includeRegisters: false, includePayloads: false }; + for (const [key, value] of Object.entries(options)) { + if (key === 'includeRegisters' || key === 'includePayloads') { + if (typeof value !== 'boolean') throw new TypeError(`${key} must be boolean`); + } else if (Object.hasOwn(CHROMATIX_LIMITS, key)) { + if (!Number.isSafeInteger(value) || value < 1 || value > CHROMATIX_LIMITS[key]) { + throw new RangeError(`${key} must be from 1 to ${CHROMATIX_LIMITS[key]}`); + } + } else { + throw new TypeError(`Unknown decoder option: ${key}`); + } + result[key] = value; + } + return result; +} + +/** + * Structure corroborated against the front IMX681 and rear OV13858 modules: + * fixed header; section 0 with 56-byte named descriptors; section 1 with their + * referenced bytes; trailing CRC32. Descriptor fields whose meaning is unknown + * remain opaque. Register rows are supported only when their named references + * and lengths agree with the observed 40-byte layout. Front mode 1/5 ordered + * pairs were independently compared with the GPL-2.0 reference data in + * https://github.com/karsies-wq/sp11-imx681-linux/tree/b08f76f40b8d7b715bd4da6aef484f86142cc147/sensor-data + * That comparison corroborates serialization, not electrical timing, hardware + * routing or successful capture. + */ +export function decodeChromatix(input, options = {}) { + if (!(input instanceof Uint8Array)) throw new TypeError('Input must be bytes'); + const config = optionsFor(options); + const fail = (code, message, offset = 0) => { + throw new ChromatixDecodeError(code, message, offset); + }; + if (input.byteLength > config.maxBytes) fail('BYTE_LIMIT', 'Input exceeds the byte limit'); + const bytes = Buffer.from(input); + if (bytes.length < 172) fail('SHORT_HEADER', 'Observed header requires at least 172 bytes'); + + function ascii(offset, length, label) { + const data = bytes.subarray(offset, offset + length); + const end = data.indexOf(0); + if (end < 0 || data.subarray(end).some((byte) => byte !== 0) || + data.subarray(0, end).some((byte) => byte < 0x20 || byte > 0x7e)) { + fail('ASCII_FIELD', `${label} must be NUL-terminated ASCII with zero padding`, offset); + } + return data.subarray(0, end).toString('ascii'); + } + + if (ascii(0, 28, 'Signature') !== 'QTI Chromatix Header') { + fail('BAD_MAGIC', 'Input does not have the observed Chromatix signature'); + } + const endOffset = bytes.readUInt32LE(28); + if (endOffset !== bytes.length - 4) { + fail('LENGTH_MISMATCH', 'Declared end excludes exactly one trailing CRC32', 28); + } + const version = { + major: bytes.readUInt16LE(32), + minor: bytes.readUInt16LE(34), + patch: bytes.readUInt32LE(36), + }; + if (version.major !== 3 || version.minor !== 0 || version.patch !== 1) { + fail('UNSUPPORTED_VERSION', 'Only the observed Chromatix 3.0.1 layout is supported', 32); + } + const storedCrc32 = bytes.readUInt32LE(endOffset); + const computedCrc32 = crc32(bytes.subarray(0, endOffset)); + if (storedCrc32 !== computedCrc32) fail('CRC_MISMATCH', 'Trailing CRC32 does not match', endOffset); + const parser = ascii(40, 48, 'Parser identification'); + const moduleName = ascii(88, 64, 'Module name'); + const tableOffset = bytes.readUInt32LE(160); + const sectionCount = bytes.readUInt32LE(164); + if (sectionCount !== 3 || tableOffset < 168 || tableOffset + sectionCount * 12 > endOffset) { + fail('SECTION_TABLE', 'Expected three complete section descriptors after the header', 160); + } + const sections = []; + const bySection = new Map(); + for (let index = 0; index < sectionCount; index++) { + const descriptorOffset = tableOffset + index * 12; + const id = bytes.readUInt32LE(descriptorOffset); + const offset = bytes.readUInt32LE(descriptorOffset + 4); + const length = bytes.readUInt32LE(descriptorOffset + 8); + if (bySection.has(id) || id > 2) fail('SECTION_ID', 'Duplicate or unsupported section ID', descriptorOffset); + if (offset < tableOffset + sectionCount * 12 || offset + length > endOffset) { + fail('SECTION_BOUNDS', 'Section lies outside the file data region', descriptorOffset + 4); + } + const section = { id, descriptorOffset, offset, bytes: length }; + sections.push(section); + bySection.set(id, section); + } + const orderedSections = [...sections].sort((a, b) => a.offset - b.offset); + for (let i = 1; i < orderedSections.length; i++) { + if (orderedSections[i].offset < orderedSections[i - 1].offset + orderedSections[i - 1].bytes) { + fail('SECTION_OVERLAP', 'Section regions overlap', orderedSections[i].descriptorOffset); + } + } + for (const section of sections) { + section.sha256 = hash(bytes.subarray(section.offset, section.offset + section.bytes)); + } + const descriptors = bySection.get(0); + const payloads = bySection.get(1); + if (descriptors.bytes % 56 !== 0) { + fail('DESCRIPTOR_LENGTH', 'Descriptor section length is not a multiple of 56', descriptors.offset); + } + const descriptorCount = descriptors.bytes / 56; + if (descriptorCount > config.maxNodes) fail('NODE_LIMIT', 'Descriptor count exceeds the limit', descriptors.offset); + + const nodes = []; + const byId = new Map(); + let totalPayloadBytes = 0; + for (let index = 0; index < descriptorCount; index++) { + const descriptorOffset = descriptors.offset + index * 56; + const id = bytes.readUInt32LE(descriptorOffset); + if (id === 0 || byId.has(id)) fail('NODE_ID', 'Descriptor IDs must be nonzero and unique', descriptorOffset); + const name = ascii(descriptorOffset + 4, 32, 'Descriptor name'); + const payloadRelativeOffset = bytes.readUInt32LE(descriptorOffset + 48); + const length = bytes.readUInt32LE(descriptorOffset + 52); + if (payloadRelativeOffset + length > payloads.bytes) { + fail('PAYLOAD_BOUNDS', 'Named descriptor payload exceeds its section', descriptorOffset + 48); + } + totalPayloadBytes += length; + if (totalPayloadBytes > config.maxTotalPayloadBytes) { + fail('PAYLOAD_LIMIT', 'Aggregate referenced payload length exceeds the limit', descriptorOffset); + } + const payloadOffset = payloads.offset + payloadRelativeOffset; + const node = { + id, name, descriptorOffset, + opaqueDescriptorWords: [36, 40, 44].map((offset) => bytes.readUInt32LE(descriptorOffset + offset)), + payloadOffset, payloadBytes: length, + sha256: hash(bytes.subarray(payloadOffset, payloadOffset + length)), + }; + if (config.includePayloads) { + node.payloadHex = bytes.subarray(payloadOffset, payloadOffset + length).toString('hex'); + } + nodes.push(node); + byId.set(id, node); + } + + let resolvedValueCount = 0; + let resolvedPayloadBytes = 0; + function referencedWords(id, expectedName, count, sourceOffset, wordBytes = 4) { + const node = byId.get(id); + if (!node || node.name !== expectedName || count > config.maxRegisterRows || node.payloadBytes !== count * wordBytes) { + fail('NAMED_REFERENCE', `Reference must identify ${expectedName} with count-matched ${wordBytes}-byte data`, sourceOffset); + } + // Charge every use, including repeated references to shared payloads. Bounds + // on unique nodes alone do not bound resolved arrays or their digest work. + resolvedValueCount += count; + resolvedPayloadBytes += count * wordBytes; + if (resolvedValueCount > config.maxResolvedValues || resolvedPayloadBytes > config.maxResolvedBytes) { + fail('RESOLUTION_LIMIT', 'Aggregate resolved reference budget exceeded', sourceOffset); + } + return { + nodeId: id, payloadOffset: node.payloadOffset, payloadBytes: node.payloadBytes, + sha256: node.sha256, + values: Array.from({ length: count }, (_, index) => bytes.readUIntLE(node.payloadOffset + index * wordBytes, wordBytes)), + }; + } + + const registerSequences = []; + const sequenceById = new Map(); + let registerRowCount = 0; + for (const node of nodes.filter((entry) => entry.name === 'regSetting')) { + if (node.payloadBytes % 40 !== 0) { + fail('REGISTER_ROW_LENGTH', 'Observed regSetting rows require 40 bytes each', node.payloadOffset); + } + const rowCount = node.payloadBytes / 40; + registerRowCount += rowCount; + if (registerRowCount > config.maxRegisterRows) { + fail('REGISTER_LIMIT', 'Register row count exceeds the limit', node.payloadOffset); + } + const rows = []; + const widths = new Map(); + const operations = new Map(); + const resolvedDigest = createHash('sha256'); + for (let index = 0; index < rowCount; index++) { + const offset = node.payloadOffset + index * 40; + const words = Array.from({ length: 10 }, (_, item) => bytes.readUInt32LE(offset + item * 4)); + const [slaveCount, slaveReference, registerAddress, dataCount, dataReference, + registerAddressBytes, registerDataBytes, operation, delayCount, delayReference] = words; + // Nonempty slaveAddr descriptors in the EEPROM records contain uint16s. + const slave = referencedWords(slaveReference, 'slaveAddr', slaveCount, offset + 4, 2); + const data = referencedWords(dataReference, 'registerData', dataCount, offset + 16); + const delay = referencedWords(delayReference, 'delayUs', delayCount, offset + 36); + if (![0, 1, 2, 3, 4].includes(registerAddressBytes) || ![0, 1, 2, 3, 4].includes(registerDataBytes)) { + fail('REGISTER_WIDTH', 'Register width code lies outside the observed 0..4 range', offset + 20); + } + // Zero-width records occur in TPG/auxiliary placeholders; retain the code + // without treating it as a usable register width. Nonzero operation codes + // remain opaque: their registerData may hold counts rather than values. + if ((registerAddressBytes > 0 && registerAddress >= 2 ** (8 * registerAddressBytes)) || + (operation === 0 && registerDataBytes > 0 && + data.values.some((value) => value >= 2 ** (8 * registerDataBytes)))) { + fail('REGISTER_VALUE_WIDTH', 'Register address or data exceeds its declared width', offset + 8); + } + const row = { + offset, registerAddress, registerAddressBytes, registerDataBytes, + widthSpecified: registerAddressBytes > 0 && registerDataBytes > 0, + operationCode: operation, + slaveReference: slave.nodeId, slaveAddresses: slave.values, + dataReference: data.nodeId, data: data.values, + delayReference: delay.nodeId, delayUs: delay.values, + }; + // Every payload reference contributes, including data outside the row span. + resolvedDigest.update(`${JSON.stringify(row)}\n`); + if (config.includeRegisters) rows.push(row); + const widthKey = `${registerAddressBytes}/${registerDataBytes}`; + widths.set(widthKey, (widths.get(widthKey) ?? 0) + 1); + operations.set(operation, (operations.get(operation) ?? 0) + 1); + } + const sequence = { + nodeId: node.id, payloadOffset: node.payloadOffset, payloadBytes: node.payloadBytes, + rowCount, rowSpanSha256: node.sha256, + resolvedRowsSha256: resolvedDigest.digest('hex'), + widths: [...widths].map(([bytes, rows]) => ({ addressDataBytes: bytes, rows })), + operationCodes: [...operations].map(([code, rows]) => ({ code, rows })), + ...(config.includeRegisters ? { rows } : {}), + }; + registerSequences.push(sequence); + sequenceById.set(node.id, sequence); + } + + const registerOwners = []; + for (const node of nodes.filter((entry) => entry.name === 'resSettings' || entry.name === 'initSetting')) { + if (node.payloadBytes !== 8) { + fail('REGISTER_OWNER_LENGTH', 'Observed register owner requires count and reference', node.payloadOffset); + } + const count = bytes.readUInt32LE(node.payloadOffset); + const referenceId = bytes.readUInt32LE(node.payloadOffset + 4); + const sequence = sequenceById.get(referenceId); + if (!sequence || sequence.rowCount !== count) { + fail('REGISTER_OWNER_REFERENCE', 'Register owner count/reference does not match its sequence', node.payloadOffset); + } + registerOwners.push({ nodeId: node.id, name: node.name, payloadOffset: node.payloadOffset, count, referenceId }); + } + + return { + schema: 'sp11-chromatix-observed-3.0.1-v1', + interpretation: 'structure and named references only; table agreement does not verify hardware behavior', + bytes: bytes.length, sha256: hash(bytes), + header: { + version, parser, moduleName, endOffset, + crc32: storedCrc32.toString(16).padStart(8, '0'), crc32Verified: true, + opaqueWords: [bytes.readUInt32LE(152), bytes.readUInt32LE(156)], + }, + stats: { + descriptorCount, totalPayloadBytes, registerSequences: registerSequences.length, + registerRowCount, resolvedValueCount, resolvedPayloadBytes, + }, + sections, nodes, registerSequences, registerOwners, + emitted: { registerValues: config.includeRegisters, payloadBytes: config.includePayloads }, + }; +} + +async function main(args) { + const options = {}; + let input; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--input' && !input && args[i + 1]) input = args[++i]; + else if (args[i] === '--include-registers' && !options.includeRegisters) options.includeRegisters = true; + else if (args[i] === '--include-payloads' && !options.includePayloads) options.includePayloads = true; + else throw new TypeError('Usage: chromatix.mjs --input FILE [--include-registers] [--include-payloads]'); + } + if (!input) throw new TypeError('An --input file is required'); + const handle = await open(input, 'r'); + let bytes; + try { + const stat = await handle.stat(); + if (!stat.isFile()) throw new TypeError('Input must be a regular file'); + if (stat.size > CHROMATIX_LIMITS.maxBytes) { + throw new ChromatixDecodeError('BYTE_LIMIT', 'Input exceeds the byte limit'); + } + bytes = Buffer.alloc(stat.size + 1); + let offset = 0; + while (offset < bytes.length) { + const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, null); + if (bytesRead === 0) break; + offset += bytesRead; + } + if (offset !== stat.size) throw new ChromatixDecodeError('FILE_CHANGED', 'Input length changed while reading'); + bytes = bytes.subarray(0, offset); + } finally { + await handle.close(); + } + process.stdout.write(`${JSON.stringify(decodeChromatix(bytes, options), null, 2)}\n`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + main(process.argv.slice(2)).catch((error) => { + process.stderr.write(`${JSON.stringify({ + error: error.code ?? error.name, message: error.message, + ...(error instanceof ChromatixDecodeError ? { offset: error.offset } : {}), + })}\n`); + process.exitCode = 1; + }); +} diff --git a/tools/sp11-driver-audit/chromatix.test.mjs b/tools/sp11-driver-audit/chromatix.test.mjs new file mode 100644 index 00000000000000..08dc657235dc6c --- /dev/null +++ b/tools/sp11-driver-audit/chromatix.test.mjs @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-2.0-only +import assert from 'node:assert/strict'; +import { mkdtemp, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; +import { crc32, decodeChromatix, ChromatixDecodeError } from './chromatix.mjs'; + +// All fixtures are independently constructed, not vendor binary excerpts. +const u32 = (...values) => { + const bytes = Buffer.alloc(values.length * 4); + values.forEach((value, index) => bytes.writeUInt32LE(value, index * 4)); + return bytes; +}; +const finish = (bytes) => { + bytes.writeUInt32LE(crc32(bytes.subarray(0, -4)), bytes.length - 4); + return bytes; +}; + +function fixture({ repeatRows = 1, dataValues = [0x56] } = {}) { + const definitions = [ + ['resSettings', u32(repeatRows, 2)], + ['regSetting', Buffer.concat(Array.from({ length: repeatRows }, () => + u32(0, 3, 0x1234, dataValues.length, 4, 2, 1, 0, 1, 5)))], + ['slaveAddr', Buffer.alloc(0)], + ['registerData', u32(...dataValues)], + ['delayUs', u32(7)], + ['uninterpreted', u32(0xffffffff)], + ]; + const table = 168; + const metadata = table + 36; + const metadataBytes = definitions.length * 56; + const data = metadata + metadataBytes; + const dataBytes = definitions.reduce((sum, item) => sum + item[1].length, 0); + const footer = data + dataBytes; + const bytes = Buffer.alloc(footer + 20 + 4); + bytes.write('QTI Chromatix Header', 0, 'ascii'); + bytes.writeUInt32LE(bytes.length - 4, 28); + bytes.writeUInt16LE(3, 32); bytes.writeUInt32LE(1, 36); + bytes.write('Parameter Parser V3.4.0 (synthetic)', 40, 'ascii'); + bytes.write('synthetic.example', 88, 'ascii'); + bytes.writeUInt32LE(1, 152); + bytes.writeUInt32LE(table, 160); bytes.writeUInt32LE(3, 164); + u32(0, metadata, metadataBytes, 1, data, dataBytes, 2, footer, 20).copy(bytes, table); + let relative = 0; + definitions.forEach(([name, payload], index) => { + const offset = metadata + index * 56; + bytes.writeUInt32LE(index + 1, offset); + bytes.write(name, offset + 4, 'ascii'); + bytes.writeUInt32LE(0xffffffff, offset + 44); + bytes.writeUInt32LE(relative, offset + 48); + bytes.writeUInt32LE(payload.length, offset + 52); + payload.copy(bytes, data + relative); + relative += payload.length; + }); + return { bytes: finish(bytes), metadata, data, footer }; +} + +function rejects(code, bytes, options) { + assert.throws(() => decodeChromatix(bytes, options), (error) => { + assert.ok(error instanceof ChromatixDecodeError); + assert.equal(error.code, code); + return true; + }); +} + +test('CRC32 has its standard independent check value', () => { + assert.equal(crc32(Buffer.from('123456789')), 0xcbf43926); +}); + +test('default summaries validate references without emitting registers or payloads', () => { + const result = decodeChromatix(fixture().bytes); + assert.equal(result.stats.descriptorCount, 6); + assert.equal(result.stats.registerRowCount, 1); + assert.equal(result.header.crc32Verified, true); + assert.equal(result.nodes[0].name, 'resSettings'); + assert.equal(result.nodes[5].opaqueDescriptorWords[2], 0xffffffff); + assert.equal('payloadHex' in result.nodes[3], false); + assert.equal('rows' in result.registerSequences[0], false); + assert.deepEqual(result.registerOwners.map((owner) => [owner.count, owner.referenceId]), [[1, 2]]); +}); + +test('explicit private-research options expose precise named values and bytes', () => { + const result = decodeChromatix(fixture().bytes, { includeRegisters: true, includePayloads: true }); + const row = result.registerSequences[0].rows[0]; + assert.equal(row.registerAddress, 0x1234); + assert.deepEqual(row.data, [0x56]); + assert.deepEqual(row.slaveAddresses, []); + assert.deepEqual(row.delayUs, [7]); + assert.equal(row.operationCode, 0); + assert.equal(result.nodes[3].payloadHex, '56000000'); + assert.deepEqual(result.emitted, { registerValues: true, payloadBytes: true }); +}); + +test('header signature, lengths, version and CRC are enforced', () => { + rejects('SHORT_HEADER', Buffer.alloc(171)); + const { bytes } = fixture(); + const magic = Buffer.from(bytes); magic[0] = 88; + rejects('BAD_MAGIC', magic); + const length = Buffer.from(bytes); length.writeUInt32LE(bytes.length, 28); + rejects('LENGTH_MISMATCH', length); + const version = Buffer.from(bytes); version.writeUInt16LE(5, 32); + rejects('UNSUPPORTED_VERSION', version); + const crc = Buffer.from(bytes); crc[crc.length - 1] ^= 1; + rejects('CRC_MISMATCH', crc); +}); + +test('section tables reject overlap, overflow, missing IDs and non-descriptor extents', () => { + const { bytes, metadata } = fixture(); + const overlap = Buffer.from(bytes); overlap.writeUInt32LE(metadata, 184); + rejects('SECTION_OVERLAP', finish(overlap)); + const overflow = Buffer.from(bytes); overflow.writeUInt32LE(0xffffffff, 176); + rejects('SECTION_BOUNDS', finish(overflow)); + const duplicate = Buffer.from(bytes); duplicate.writeUInt32LE(0, 180); + rejects('SECTION_ID', finish(duplicate)); + const count = Buffer.from(bytes); count.writeUInt32LE(0xffffffff, 164); + rejects('SECTION_TABLE', finish(count)); + const extent = Buffer.from(bytes); extent.writeUInt32LE(55, 176); + rejects('DESCRIPTOR_LENGTH', finish(extent)); +}); + +test('named descriptors enforce ASCII, unique IDs and payload bounds', () => { + const { bytes, metadata } = fixture(); + const name = Buffer.from(bytes); name[metadata + 4] = 0xff; + rejects('ASCII_FIELD', finish(name)); + const duplicate = Buffer.from(bytes); duplicate.writeUInt32LE(1, metadata + 56); + rejects('NODE_ID', finish(duplicate)); + const overflow = Buffer.from(bytes); overflow.writeUInt32LE(0xffffffff, metadata + 48); + rejects('PAYLOAD_BOUNDS', finish(overflow)); +}); + +test('register payload references, owner counts and value widths are validated', () => { + const { bytes, metadata, data } = fixture(); + const missing = Buffer.from(bytes); missing.writeUInt32LE(99, data + 8 + 16); + rejects('NAMED_REFERENCE', finish(missing)); + const wrongName = Buffer.from(bytes); wrongName.writeUInt32LE(5, data + 8 + 16); + rejects('NAMED_REFERENCE', finish(wrongName)); + const oversizedCount = Buffer.from(bytes); oversizedCount.writeUInt32LE(0xffffffff, data + 8 + 12); + rejects('NAMED_REFERENCE', finish(oversizedCount)); + const owner = Buffer.from(bytes); owner.writeUInt32LE(2, data); + rejects('REGISTER_OWNER_REFERENCE', finish(owner)); + const width = Buffer.from(bytes); width.writeUInt32LE(5, data + 8 + 20); + rejects('REGISTER_WIDTH', finish(width)); + const value = Buffer.from(bytes); value.writeUInt32LE(0x100, data + 8 + 40); + rejects('REGISTER_VALUE_WIDTH', finish(value)); + const size = Buffer.from(bytes); size.writeUInt32LE(39, metadata + 56 + 52); + rejects('REGISTER_ROW_LENGTH', finish(size)); +}); + +test('zero width and unknown operations remain opaque instead of executable policy', () => { + const { bytes, data } = fixture(); + const placeholder = Buffer.from(bytes); + placeholder.writeUInt32LE(0, data + 8 + 20); + placeholder.writeUInt32LE(0, data + 8 + 24); + const row = decodeChromatix(finish(placeholder), { includeRegisters: true }).registerSequences[0].rows[0]; + assert.equal(row.widthSpecified, false); + const opaqueOperation = Buffer.from(bytes); + opaqueOperation.writeUInt32LE(77, data + 8 + 28); + opaqueOperation.writeUInt32LE(0xffffffff, data + 8 + 40); + const unknown = decodeChromatix(finish(opaqueOperation), { includeRegisters: true }).registerSequences[0].rows[0]; + assert.equal(unknown.operationCode, 77); + assert.deepEqual(unknown.data, [0xffffffff]); +}); + +test('resource and option bounds cannot be expanded or bypassed', () => { + const { bytes } = fixture(); + rejects('BYTE_LIMIT', bytes, { maxBytes: 10 }); + rejects('NODE_LIMIT', bytes, { maxNodes: 5 }); + rejects('PAYLOAD_LIMIT', bytes, { maxTotalPayloadBytes: 1 }); + assert.throws(() => decodeChromatix(bytes, { includeRegisters: 'yes' }), TypeError); + assert.throws(() => decodeChromatix(bytes, { maxNodes: 100001 }), RangeError); + assert.throws(() => decodeChromatix(bytes, { toString: 1 }), TypeError); +}); + +test('reused payload references consume the aggregate resolution budget every time', () => { + const { bytes } = fixture({ repeatRows: 3, dataValues: Array(100).fill(0x56) }); + // Only one 400-byte data node exists, but three rows resolve it repeatedly. + const result = decodeChromatix(bytes); + assert.equal(result.stats.resolvedValueCount, 303); + assert.equal(result.stats.resolvedPayloadBytes, 1212); + rejects('RESOLUTION_LIMIT', bytes, { maxResolvedValues: 150 }); + rejects('RESOLUTION_LIMIT', bytes, { maxResolvedBytes: 600 }); + rejects('RESOLUTION_LIMIT', bytes, { maxResolvedValues: 150, includeRegisters: true }); + assert.throws(() => decodeChromatix(bytes, { maxResolvedValues: 1_000_001 }), RangeError); +}); + +test('CLI default and opt-in output follow the same disclosure boundaries', async () => { + const temporary = await mkdtemp(join(tmpdir(), 'sp11-chromatix-test-')); + try { + const input = join(temporary, 'synthetic.bin'); + await writeFile(input, fixture().bytes); + const cli = fileURLToPath(new URL('./chromatix.mjs', import.meta.url)); + for (const flags of [[], ['--include-registers']]) { + const run = spawnSync(process.execPath, [cli, '--input', input, ...flags], { encoding: 'utf8' }); + assert.equal(run.status, 0, run.stderr); + const decoded = JSON.parse(run.stdout); + assert.equal('rows' in decoded.registerSequences[0], flags.length > 0); + } + await writeFile(input, Buffer.alloc(4)); + const bad = spawnSync(process.execPath, [cli, '--input', input], { encoding: 'utf8' }); + assert.equal(bad.status, 1); + assert.equal(bad.stdout, ''); + assert.equal(JSON.parse(bad.stderr).error, 'SHORT_HEADER'); + } finally { + await rm(temporary, { recursive: true, force: true }); + } +}); From dd9d66719c9920b01fd550e3d4e83f63eed374af Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 11:55:22 +0100 Subject: [PATCH 03/17] tools: record native OV13858 qualification and CCI address mapping Document the native sensor foundation, source-only checks and remaining Denali wiring, module power and capture gates. Record the verified platform HAL conversion into the native CCI seven-bit address field without guessing firmware controller ordering. Preserve raw sensor metadata and add regression coverage for the converted field. --- tools/sp11-driver-audit/README.md | 7 +- tools/sp11-driver-audit/camera-config.mjs | 5 +- .../sp11-driver-audit/camera-config.test.mjs | 2 + tools/sp11-driver-audit/camera-fields.md | 19 ++++ tools/sp11-driver-audit/ov13858-native.md | 87 +++++++++++++++++++ 5 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 tools/sp11-driver-audit/ov13858-native.md diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index c11fe250c8a389..4596b581f7ca81 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -88,7 +88,7 @@ registers. Neither check establishes CSI timing or successful rear capture. | Track | Existing Linux position | Next implementation evidence | | --- | --- | --- | | Front IMX681 | Native driver and OLED graph; prior preview and indicator tests | Compare verified power, lane and stream/error lifecycle with this package | -| Rear OV13858 | Native sensor driver assumes an ACPI-powered device; no Denali graph | Native power resources, module-specific PHY/modes, actuator and indicator ownership | +| Rear OV13858 | Native OF power-management foundation under development; no Denali graph | Verified board resources, module-specific PHY/modes, actuator and indicator ownership | | IR VD55G0/VD55G1 | Different sensors and board variants; support differs | Exact module identity, native driver, transport and illumination limits | | Touch/pen | Native transport and userspace exist; fresh-boot autoload repair is a separate series | Preserve repair and investigate actual lifecycle gaps | | Suspend | Several device and SoC resource owners are involved | Decode power policies and compare failed Linux transitions with measured Windows behaviour | @@ -100,6 +100,11 @@ Hardware identity comes from the actual board, firmware tables and device identification. The bundle includes optional docks, modems, SSDs, panels and camera assemblies; membership in the package does not justify enabling them. +The [native OV13858 series](ov13858-native.md) is separate from these research +tools. It uses Linux resource interfaces, leaves existing sensor tables intact +and does not load the inspected configuration files. Its board-enablement and +hardware-qualification gates remain explicit. + The research checkout began at the older integration revision `2cbd1ec3e2da385e7bd91fd65c63ba5a8fb5b865`. These tools are independent of that kernel revision and are fork research infrastructure. Hardware changes must diff --git a/tools/sp11-driver-audit/camera-config.mjs b/tools/sp11-driver-audit/camera-config.mjs index d8ef1d82c8c156..e534c215df54a6 100644 --- a/tools/sp11-driver-audit/camera-config.mjs +++ b/tools/sp11-driver-audit/camera-config.mjs @@ -29,7 +29,8 @@ export function decodeSensorConfiguration(input) { schema:'sp11-camera-scfg-observed-v1',sourceSHA256:decoded.sha256, names:{sensorModule:fields[0].value,tuning:fields[1].value}, bus:{offset:fields[2].payloadOffset,packed:`0x${bus.toString(16).padStart(8,'0')}`, - windowsSlaveAddress:bus&0xffff,observedInterfaceAddressByte:bus&0xff,frequencyCode, + windowsSlaveAddress:bus&0xffff,observedInterfaceAddressByte:bus&0xff, + observedCciSlaveId:(bus>>>1)&0x7f,frequencyCode, observedFrequencySetting:[100,400,1000,400][frequencyCode], registerAddressTypeCode,registerDataTypeCode, observedAddressType:[1,2,3,1][registerAddressTypeCode], @@ -38,7 +39,7 @@ export function decodeSensorConfiguration(input) { probe:{offset:fields[3].payloadOffset,register:probe&0xffff,expectedValue:probe>>>16}, uninterpretedTail:{offset:fields[4].payloadOffset,value:tail}, limits:['Names are labels only; no referenced file is opened.', - 'The raw Windows slave-address field is not a proven Linux seven-bit address.', + 'The raw Windows field is not a Linux address; the observed platform HAL shifts its interface byte right one and masks seven bits.', 'Frequency setting units and CCI bus mapping are not established by this consumer.', 'Enum code three uses a logged Windows fallback; it does not establish hardware capability.', 'Observed type two uses a two-byte register address and a big-endian two-byte probe value.', diff --git a/tools/sp11-driver-audit/camera-config.test.mjs b/tools/sp11-driver-audit/camera-config.test.mjs index 197e9e3062ac54..bea109000b611d 100644 --- a/tools/sp11-driver-audit/camera-config.test.mjs +++ b/tools/sp11-driver-audit/camera-config.test.mjs @@ -74,6 +74,7 @@ function sensorFixture(values=[0x00150020,0x12345678,0],names=['sensor-label','t test('sensor probe fields separate Windows address, enum codes and identity',()=>{ const decoded=decodeSensorConfiguration(sensorFixture()); assert.equal(decoded.bus.windowsSlaveAddress,0x20); + assert.equal(decoded.bus.observedCciSlaveId,0x10); assert.equal(decoded.bus.frequencyCode,1); assert.equal(decoded.bus.observedFrequencySetting,400); assert.equal(decoded.bus.observedAddressType,2); @@ -96,6 +97,7 @@ test('sensor labels stay inert and opaque fields and enum fallback remain visibl const wide=decodeSensorConfiguration(sensorFixture([0x00151234,0,0])); assert.equal(wide.bus.windowsSlaveAddress,0x1234); assert.equal(wide.bus.observedInterfaceAddressByte,0x34); + assert.equal(wide.bus.observedCciSlaveId,0x1a); }); test('sensor interpreter rejects unverified extensions, missing labels and wide fields',()=>{ diff --git a/tools/sp11-driver-audit/camera-fields.md b/tools/sp11-driver-audit/camera-fields.md index ba556dd2a4fab7..bfa68061e94e71 100644 --- a/tools/sp11-driver-audit/camera-fields.md +++ b/tools/sp11-driver-audit/camera-fields.md @@ -100,6 +100,25 @@ The 16-bit stored field is copied into a byte in the inspected interface; both the stored field and that byte are reported without normalizing either. The tool retains names as inert labels and opens no referenced paths. +### Platform CCI conversion + +Further analysis resolves the address conversion outside those consumers. +In the platform binary identified at the start of this document, command +builder `0x14000f838` extracts bits 1–7 of the raw interface byte; instruction +`0x14000f85c` is the corresponding `ubfx`. It places the result in the CCI +SET_PARAM address field. Native `drivers/i2c/busses/i2c-qcom-cci.c` puts its +seven-bit Linux address into the same field without that shift. Consequently +raw `0x20` corresponds to CCI/Linux address `0x10`, not Linux address `0x20`. +The interpreter reports that observed conversion alongside both raw fields. + +Controller routing remains a separate boundary. Initialization at +`0x14000a988` splits a nonnegative logical master index into controller +instance `index / 2` and master `index % 2`. However, `0x14003b850` obtains +controller bases dynamically from translated firmware resource ordinals two +and three. Matching those resources to Linux CCI0 at `0x0ac15000` and CCI1 at +`0x0ac16000` requires the actual platform's resource descriptors. The split +alone does not prove that Windows instance zero is Linux `cci0`. + ## Power-resource delays Static analysis of `qcpep8380.sys`, SHA-256 diff --git a/tools/sp11-driver-audit/ov13858-native.md b/tools/sp11-driver-audit/ov13858-native.md new file mode 100644 index 00000000000000..a960e23b878f4e --- /dev/null +++ b/tools/sp11-driver-audit/ov13858-native.md @@ -0,0 +1,87 @@ +# Native OV13858 foundation + +This development series extends the existing Linux OV13858 sensor driver. +It requires no Windows installation, MSI, vendor configuration container, +Windows executable or recovered register table to build or run. It is not yet +a working rear-camera configuration for ARM Surface Pro 11. + +## Scope + +The generic `ovti,ov13858` binding supplies a native firmware-node interface: +one 19.2 MHz clock, `dovdd`, `avdd` and `dvdd` supplies, an optional reset GPIO +and a CSI-2 endpoint. Existing ACPI systems retain their power-domain resource +ownership and do not acquire the new DT supplies or GPIO. + +The driver retains its existing register tables. The endpoint selects the +existing 270 MHz modes, the existing 540 MHz mode, or both. Mode enumeration, +format selection and controls use that selected set. Four D-PHY data lanes +in order, clock lane zero, normal polarities and continuous clock are required; +unsupported configurations are rejected before power resources are used. + +Runtime PM is enabled before asynchronous subdevice registration, with a +reference held through publication. Unknown supply state after enable, +disable or unwind failure latches a fatal power fault. Reset remains asserted, +the enabled clock is stopped once, and further register I/O or stream enable +is rejected. This is containment, not automatic electrical recovery: a reprobe +alone cannot establish that a failed regulator provider or physical rail has +recovered. + +## Source provenance + +Power sequencing follows native Linux regulator, clock, GPIO and V4L2 APIs. +The 1 ms reset hold and 5 ms post-release recovery use the precedent in +[Sergey Lebedev's Linux-media v3 proposal](https://lore.kernel.org/linux-media/20260831130312.26296-1-lsa.uz@pm.me/). +That work concerns Intel Surface Pro 11 for Business; these times are not +measurements on ARM Denali. Its contribution is referenced rather than +attributing this implementation or a new sign-off to that author. + +Separate static research establishes milliseconds for the Windows resource +`DELAY` values, including 1 and 10 ms in the rear configuration. This does not +prove that the generic sensor timing covers the entire Denali module. Do not +silently substitute either sequence for verified board/module requirements. + +## Before Denali enablement + +No Denali DTS node is enabled by this series. Resolve and review: + +- Linux CCI controller and bus. The platform HAL now establishes raw Windows + address `0x20` as CCI/Linux address `0x10`, but logical master indices still + need matching to the actual firmware resource ordering. +- Reset routing and polarity, supply identities and ordering, including the + separate 2.9 V module/actuator resource. +- Physical CSI lane assignment, link frequency and module-specific mode + timing. Windows metadata and programmed frame length currently disagree; + output-size agreement is not enough to infer clock timing. +- Actuator/OTP calibration ownership and indicator sharing with the front + and IR cameras, using native Linux interfaces. + +Runtime qualification must cover chip identification, test-pattern/capture +layout, start/stop/reopen, fault and probe-deferral paths, indicator release, +idle power, system sleep, and existing ACPI users. Source compilation and +binding validation are not evidence that rear capture works. + +## Source checks completed + +The source is split into binding commit `82d8210aa474` and native driver commit +`135b49d9a108`. The existing driver blob was identical in this research base, +the tested v8 carrier and the inspected jglathe 7.2.9 tree before these edits. + +- ARM64 GCC 13.3 `W=1` driver-object builds passed with ACPI enabled, ACPI + disabled, and runtime PM disabled in a minimal configuration. +- Targeted binding/example validation passed, with positive frequency-subset + fixtures and negative lane, polarity, clock and frequency fixtures. +- Strict checkpatch reported zero errors, warnings or checks. +- Seven host logic scenarios using the exact extracted power, register and + stream-guard functions passed AddressSanitizer and UndefinedBehaviorSanitizer. + These use API stubs, not the real PM core, regulators or hardware. +- An OpenCode GLM 5.1 final source review found no source-level blocker after + its initial findings were checked against this tree and the genuine unknown + supply-state failure was contained. +- Both focused commits applied cleanly to an exact tested-v8 source export. + Its exported `qcom-x1e` configuration already selected OV13858 as a module; + the ARM64 `W=1` driver-object build passed against those carrier headers, + with no Windows package present in the build environment. + +These are source and driver-object checks, not a newly qualified combined +kernel. Full kernel/module linkage, integration with later carrier changes +and physical OF/ACPI regression tests remain required before a release claim. From c1aeebbcf7a48bfd9de0d0cd37d8d8f751d9fcab Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 12:19:29 +0100 Subject: [PATCH 04/17] tools: decode SP11 power votes and captured camera resources Retain verified GPIO, voltage-vote and delay fields without inferring sensor pin ownership or generating production policy. Keep unknown resources opaque and bound repeated wrapper output. Add a strictly selected ARM64 resource-list reader with synthetic tests, and document the saved allocation addresses alongside the independently verified CCI consumer. Identical capture copies remain one observation. Signed-off-by: Leon Silcott --- tools/sp11-driver-audit/README.md | 29 +++ tools/sp11-driver-audit/camera-fields.md | 97 ++++++- tools/sp11-driver-audit/camera-power.mjs | 181 +++++++++++++ tools/sp11-driver-audit/camera-power.test.mjs | 173 +++++++++++++ tools/sp11-driver-audit/ov13858-native.md | 20 +- tools/sp11-driver-audit/windows-resources.mjs | 243 ++++++++++++++++++ .../windows-resources.test.mjs | 217 ++++++++++++++++ 7 files changed, 947 insertions(+), 13 deletions(-) create mode 100644 tools/sp11-driver-audit/camera-power.mjs create mode 100644 tools/sp11-driver-audit/camera-power.test.mjs create mode 100644 tools/sp11-driver-audit/windows-resources.mjs create mode 100644 tools/sp11-driver-audit/windows-resources.test.mjs diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index 4596b581f7ca81..c5ea37fa42584f 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -26,6 +26,10 @@ node tools/sp11-driver-audit/camera-config.mjs \ --kind pcfg --input /path/to/CAMP_PCFG_MSHW0495.bin node tools/sp11-driver-audit/camera-config.mjs \ --kind scfg --input /path/to/SCFG_REAR_MSHW0491.bin +node tools/sp11-driver-audit/camera-power.mjs \ + --input /path/to/CAMS_RES_MSHW0491.bin +node tools/sp11-driver-audit/windows-resources.mjs \ + --format registry-utf16-arm64 --input /path/to/camera-platform.reg node tools/sp11-driver-audit/chromatix.mjs \ --input /path/to/com.surface.sensormodule.rfc_ov13858.bin node --test tools/sp11-driver-audit/*.test.mjs @@ -52,6 +56,22 @@ It preserves both platform configuration banks, raw values and unknown tail fields. It does not select a board, turn orientation codes into angles, or translate Windows resource indices into Linux GPIO or CCI numbers. +The power interpreter names the verified TLMM GPIO fields, type-one PMIC +voltage/enable/mode votes and resource-delay units. Unknown mechanisms, +component subtrees and extension fields remain opaque. Wrapper metadata is +stored once, with offset references from resources, so long device labels are +not repeated for every operation. Configured sequence order is not measured +electrical timing, and a GPIO level or voltage is not proof of a sensor pin's +function or ownership. + +The Windows resource reader explicitly selects the observed ARM64 packed +layout; it does not autodetect architectures or evaluate firmware. It accepts +bounded UTF-16LE registry exports containing typed `hex(8)` lists, or a raw +list with `--format cm-arm64`. Unknown layouts, variable-length resource +types, MSI interrupts and ambiguous/duplicate registry value names fail +explicitly. Registry output can contain device-instance identifiers: retain +it privately. Nothing is imported into the registry or applied to hardware. + The Chromatix reader validates the observed 3.0.1 container, CRC32, section extents, named descriptors, register-sequence references and resource budgets. It does not execute operations or emit Linux driver code. Default output @@ -76,6 +96,15 @@ arguments require the minimum data union; the earlier narrow reader rejected decoding does not establish the meaning of ADC resources or coverage of every possible argument encoding. +The new power interpreter accepts all 13 inspected camera-resource files; +uninterpreted component subtrees remain opaque rather than being treated as +covered policies. The rear file exposes three GPIO, eight PMIC-vote and three +delay records. A saved MSHW0495 camera-platform allocation independently +matches the two CCI memory ranges in Linux. Its four byte-identical export +copies constitute one observation, not four hardware trials; allocation data +is not itself a translated WDF resource trace. See the field specification +for the combined evidence and its limits. + The sensor-container reader validates the inspected IMX681, OV13858 and three OV02C10 containers. The front module has 3,035 named descriptors and 915 register rows; the rear has 2,117 descriptors and 632 rows. Some TPG and IR records use diff --git a/tools/sp11-driver-audit/camera-fields.md b/tools/sp11-driver-audit/camera-fields.md index bfa68061e94e71..eb80f0315886c5 100644 --- a/tools/sp11-driver-audit/camera-fields.md +++ b/tools/sp11-driver-audit/camera-fields.md @@ -60,10 +60,10 @@ are not degrees, Windows I2C indices are not a proven Linux bus mapping, and neither CSI lane layout nor link frequency follows from these fields. The MSHW0495 bank-zero words begin `0x00110000`, `0x00230010`, `0x01000110`. -The second word's CSIPHY 2 agrees with the existing front-camera graph. Its -Windows master 3 still needs an independent mapping to Linux `cci1_i2c1`. -The PHY match is corroboration; it does not alone prove the identities of every -sensor slot or justify enabling the rear or IR hardware. +The second word's CSIPHY 2 agrees with the existing front-camera graph. The +combined CCI evidence below now maps logical master 3 to Linux `cci1_i2c1` +on the captured platform. The PHY match alone does not prove the identities +of every sensor slot or justify enabling the rear or IR hardware. ## SCFG @@ -111,13 +111,47 @@ seven-bit Linux address into the same field without that shift. Consequently raw `0x20` corresponds to CCI/Linux address `0x10`, not Linux address `0x20`. The interpreter reports that observed conversion alongside both raw fields. -Controller routing remains a separate boundary. Initialization at +Controller routing is a separate step. Initialization at `0x14000a988` splits a nonnegative logical master index into controller instance `index / 2` and master `index % 2`. However, `0x14003b850` obtains controller bases dynamically from translated firmware resource ordinals two -and three. Matching those resources to Linux CCI0 at `0x0ac15000` and CCI1 at -`0x0ac16000` requires the actual platform's resource descriptors. The split -alone does not prove that Windows instance zero is Linux `cci0`. +and three. The split alone does not prove their Linux identities. + +### Captured controller resources + +A saved `ACPI\QCOM0C32` / MSHW0495 registry export supplies the missing +allocation addresses. Its SHA-256 is +`a46ebcfc358440fc30c196f9dc0c14b028f06a197bd6964bcd93b374b592f061`. +`BootConfig` and `AllocConfig` contain the same 200-byte resource list, +SHA-256 `f340bd8f69c8e52ce6bfcf82e6912eeea896c0c4a86ba405d5c75815bca9f857`. +Four byte-identical copies are one observation. + +The observed list has one full descriptor, version/revision 1/1 and nine +20-byte partial descriptors. Absolute ordinal 2 is memory at `0x0ac15000`, +length `0x1000`; ordinal 3 is memory at `0x0ac16000`, length `0x1000`. +Those match `cci0` and `cci1` in `hamoa.dtsi`. IRQ vectors 492 and 303 also +agree with the respective Linux GIC SPI 460 and 271 plus the architectural +32 offset. Combining that capture with the independently verified ordinal +consumer and index split gives this controller map: + +| Logical master | Captured controller memory | Linux bus | +| --- | --- | --- | +| 0 | `0x0ac15000` | `cci0_i2c0` | +| 1 | `0x0ac15000` | `cci0_i2c1` | +| 2 | `0x0ac16000` | `cci1_i2c0` | +| 3 | `0x0ac16000` | `cci1_i2c1` | + +These are captured registry allocations, not an evaluated `_CRS` or a direct +WDF translated-list dump. Do not present them as either, infer physical sensor +presence from them, or assume a different board revision has identical wiring. +Associating a PCFG slot with a sensor still requires the sensor-ID lookup +chain; controller mapping alone is not board enablement. + +The bounded reader follows the documented +[resource-list structure](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_cm_resource_list) +and [partial-descriptor fields](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_cm_partial_resource_descriptor), +with an explicit observed-layout selection. It never applies resources or +turns connection IDs into Linux GPIO numbers. ## Power-resource delays @@ -142,6 +176,53 @@ fixed 20-microsecond polling and named sensor-mode delay fields are separate. These observations do not establish supply mapping, regulator ownership or safe Linux sequencing by themselves. +## GPIO and regulator resource fields + +Further instruction-level PEP analysis establishes the mandatory six-field +`TLMMGPIO` payload. Parsing at `0x1404100e0`, conversion at `0x14040a2f0` +and construction at `0x14041fe70` preserve these fields into the runtime +node consumed by `0x14011d290`: + +| Field | Observed meaning | +| --- | --- | +| 0 | Physical TLMM GPIO number | +| 1 | Raw output level, not a logical polarity flag | +| 2 | Function/mux encoding | +| 3 | Output-enable encoding | +| 4 | Pull encoding | +| 5 | Drive-strength encoding | + +Instructions `0x1402082e4..0x14020833c` clear or set output bit 1 at the +GPIO's I/O register. The rear tuples select GPIO110, physical low then high, +and low during power-down. They select GPIO mux, output enabled, no pull +and a drive encoding equivalent to native 2 mA. There is no inversion flag +in these six fields. This does not independently prove that the net is +OV13858 reset rather than another module enable. + +For observed `PMICVREGVOTE` input type 1, `0x140440af0` selects internal +type zero and `0x140412cb0` reads voltage, software-enable, mode and a fourth +vote field. The fourth field and optional extensions remain uninterpreted. +The native service callback chain reaches `0x1402271c0`, which emits voltage +divided by 1,000, enable and mode into the same RPMh fields used by Linux. +The resource-name table maps the rear votes to `ldom6` at 1.8 V, `ldom1` +at 1.2 V, `ldom5` at 2.8 V and `ldob16` at 2.9 V. Off tuples remove that +consumer's voltage and enable votes; shared physical rails may remain on. + +The configured rear order holds GPIO110 low, votes the first three rails, +delays 1 ms, votes the 2.9 V rail, enables MCLK1 at 19.2 MHz, drives GPIO110 +high and delays 10 ms. Power-down configures low, delays 1 ms, stops MCLK1 +and withdraws the rail votes in reverse order. This is configured order, +not measured electrical timing or exclusive ownership. In particular, +voltage does not prove sensor pin roles or that `ldob16` powers autofocus. + +Linux TLMM pinctrl/GPIO and RPMh regulator providers already express these +operations. A production driver must use those frameworks, not a Windows +resource interpreter or a raw-MMIO compatibility layer. Native +`regulator_bulk_enable()` dispatches asynchronously: array order must not be +mistaken for serialization. Any required sensor-specific order needs a +reviewed native sequence and tracked error unwind after pin/rail ownership +and timing are established. + References: [ACPI output-buffer layout](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/acpiioct/ns-acpiioct-_acpi_eval_output_buffer_v1), [argument storage](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/acpiioct/ns-acpiioct-_acpi_method_argument_v1), [relative sleep units](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kedelayexecutionthread), diff --git a/tools/sp11-driver-audit/camera-power.mjs b/tools/sp11-driver-audit/camera-power.mjs new file mode 100644 index 00000000000000..a83ca56ee30c34 --- /dev/null +++ b/tools/sp11-driver-audit/camera-power.mjs @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Research-only, original decoder of statically observed resource fields. +// This never executes a driver or generates Linux hardware configuration. +import {decodeAeob, readAeobBytes} from './aeob.mjs'; +import {resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +export const POWER_FIELD_EVIDENCE = Object.freeze({ + driver: 'qcpep8380.sys', + sha256: 'e5bdeb8c0fb8823990f7520a19c00807711cf909f91f8d67db78ffb18c1b2277', + provider: 'REA 6.5.0 / Ghidra 12.1.4', + tlmm: Object.freeze({ + parser: '0x1404100e0', + conversion: '0x14040a2f0', + construction: '0x14041fe70', + executor: '0x14011d290', + physicalLevelInstructions: '0x1402082e4..0x14020833c', + nativePinLayoutBlob: '8d2b8246170b4dc4cdc63f16ada344f3b9c9bec1', + nativePinConfigBlob: 'fd0add4e71da14d6e74b0517e1015926c6c15d1b', + }), + pmic: Object.freeze({ + parser: '0x140412cb0', + inputVoteTypeConversion: '0x140440af0', + executor: '0x140126b90', + rpmhConversion: '0x1402271c0', + voltageEnableModeInstructions: '0x140227a04..0x140227a30', + nativeRegulatorBlob: '73856e3df337add151ce1bb9ce9ce4b9a836384c', + }), + delay: Object.freeze({ + parser: '0x140414680', + executor: '0x140128170', + sleepConversionInstructions: '0x140033704..0x140033710', + stallConversionInstructions: '0x140128c5c..0x140128c7c', + }), +}); + +class PowerDecodeError extends Error { + constructor(message, offset) { + super(message); + this.name = 'PowerDecodeError'; + this.code = 'POWER_FIELDS'; + this.offset = offset; + } +} + +function fail(message, node) { + throw new PowerDecodeError(message, node.offset); +} + +function uint32(node, name) { + if (!node || node.kind !== 0 || BigInt(node.value) > 0xffffffffn) { + fail(`${name} requires a 32-bit unsigned integer`, node ?? {offset: 0}); + } + return Number(node.value); +} + +function payload(tuple, minimum) { + const fields = tuple.children; + if (fields.length !== 2 || fields[1].kind !== 3 || fields[1].children.length < minimum) { + fail(`${fields[0].value} requires a nested payload with at least ${minimum} fields`, tuple); + } + return fields[1].children; +} + +function opaque(tuple, reason, context) { + return {offset: tuple.offset, interpretation: 'opaque', reason, contextOffsets: context, raw: tuple}; +} + +function decodeResource(tuple, context) { + const mechanism = tuple.children[0].value; + if (mechanism === 'TLMMGPIO') { + const fields = payload(tuple, 6); + const values = fields.slice(0, 6).map((field, index) => uint32(field, `TLMMGPIO field ${index}`)); + const [gpioNumber, rawOutputLevel, functionCode, outputEnableCode, pullCode, driveCode] = values; + // Do not silently emulate the executor's bit masks on unsupported values. + if (gpioNumber > 1023 || rawOutputLevel > 1 || functionCode > 15 || + outputEnableCode > 1 || pullCode > 3 || driveCode > 7) { + return opaque(tuple, 'TLMMGPIO values outside the interpreted native field ranges', context); + } + return { + mechanism, offset: tuple.offset, contextOffsets: context, interpretation: 'observed-fields', + gpioNumber, rawOutputLevel, functionCode, outputEnableCode, pullCode, driveCode, + equivalentDriveStrengthMilliamp: (driveCode + 1) * 2, + fields: fields.slice(0, 6), opaqueTail: fields.slice(6), + }; + } + if (mechanism === 'PMICVREGVOTE') { + const fields = payload(tuple, 2); + if (fields[0].kind !== 1) fail('PMICVREGVOTE resource name requires a string', fields[0]); + const voteTypeCode = uint32(fields[1], 'PMICVREGVOTE vote type'); + if (voteTypeCode !== 1) return opaque(tuple, 'Uninterpreted PMICVREGVOTE vote type', context); + if (fields.length < 6) fail('PMICVREGVOTE type 1 requires six payload fields', tuple); + const [voltageMicrovolts, softwareEnableVote, hardwareModeCode] = fields.slice(2, 5) + .map((field, index) => uint32(field, `PMICVREGVOTE field ${index + 2}`)); + uint32(fields[5], 'PMICVREGVOTE opaque fourth vote field'); + if (softwareEnableVote > 1) { + return opaque(tuple, 'Uninterpreted non-boolean software enable vote', context); + } + return { + mechanism, offset: tuple.offset, contextOffsets: context, interpretation: 'observed-fields', + resourceName: fields[0].value, voteTypeCode, observedInternalVoteType: 0, + voltageMicrovolts, softwareEnableVote, hardwareModeCode, + opaqueFourthVoteField: fields[5], opaqueTail: fields.slice(6), + fields: fields.slice(0, 5), + }; + } + if (mechanism === 'DELAY') { + const fields = payload(tuple, 1); + return { + mechanism, offset: tuple.offset, contextOffsets: context, interpretation: 'observed-fields', + configuredMilliseconds: uint32(fields[0], 'DELAY duration'), + durationField: fields[0], opaqueTail: fields.slice(1), + }; + } + return opaque(tuple, 'Uninterpreted resource mechanism or container', context); +} + +export function decodeCameraPower(input) { + // Bounds, recursion depth, integers, TLV lengths, counts and string encoding + // are checked by the shared structural reader before field interpretation. + const source = decodeAeob(input); + if (!source.records.length || source.records.some(node => node.kind !== 3)) { + throw new PowerDecodeError('Expected one or more camera power tuples', 12); + } + const resources = []; + const wrappers = []; + function walk(tuple, context) { + const fields = tuple.children; + if (!fields.length || fields[0].kind !== 1) { + resources.push(opaque(tuple, 'Tuple does not start with a string label', context)); + return; + } + const tag = fields[0].value; + if (tag === 'DEVICE' || tag === 'DSTATE') { + // Both observed DEVICE wrappers (with or without a numeric identifier) + // retain labels only; no platform identity or runtime policy is selected. + const prefix = tag === 'DEVICE' && fields[1]?.kind === 0 ? 3 : 2; + if (fields.length < prefix || + (tag === 'DEVICE' ? fields[prefix - 1].kind !== 1 : fields[1].kind !== 0) || + fields.slice(prefix).some(node => node.kind !== 3)) { + fail(`${tag} wrapper has an unsupported field layout`, tuple); + } + if (tag === 'DSTATE' || prefix === 3) uint32(fields[1], `${tag} ordinal`); + wrappers.push({tag, offset: tuple.offset, parentOffset: context.at(-1) ?? null, + fields: fields.slice(1, prefix)}); + const nextContext = [...context, tuple.offset]; + for (const child of fields.slice(prefix)) walk(child, nextContext); + return; + } + resources.push(decodeResource(tuple, context)); + } + for (const record of source.records) walk(record, []); + return { + schema: 'sp11-camera-power-observed-v1', sourceSHA256: source.sha256, + evidence: POWER_FIELD_EVIDENCE, wrappers, resources, + limits: [ + 'Research only: no executable, referenced path, device or configuration is opened or applied.', + 'Tuple order is configured order, not measured timing or proof of exclusive resource ownership.', + 'Raw GPIO levels do not establish reset identity or a Linux active-low flag.', + 'Rail names are inert labels; voltage does not establish sensor-core or autofocus wiring.', + 'Enable and voltage are consumer votes; a zero vote does not prove a shared rail is electrically off.', + 'DELAY units apply only to this observed resource path, not other sensor delay fields.', + 'Unknown mechanisms, vote types and extensions remain opaque; no Linux device-tree policy is generated.', + 'Native regulator_bulk_enable is asynchronous; its array order does not reproduce a serialized resource list.', + ], + }; +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + const args = process.argv.slice(2); + if (args.length !== 2 || args[0] !== '--input' || !args[1]) { + throw new TypeError('Usage: node camera-power.mjs --input FILE'); + } + console.log(JSON.stringify(decodeCameraPower(await readAeobBytes(args[1])), null, 2)); + } catch (error) { + console.error(JSON.stringify({error: error.code ?? error.name, message: error.message, + ...(error.offset === undefined ? {} : {offset: error.offset})})); + process.exitCode = 1; + } +} diff --git a/tools/sp11-driver-audit/camera-power.test.mjs b/tools/sp11-driver-audit/camera-power.test.mjs new file mode 100644 index 00000000000000..655bd2383bd01f --- /dev/null +++ b/tools/sp11-driver-audit/camera-power.test.mjs @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-only +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {decodeCameraPower, POWER_FIELD_EVIDENCE} from './camera-power.mjs'; + +function record(kind, payload) { + const bytes = Buffer.alloc(4 + Math.max(4, payload.length)); + bytes.writeUInt16LE(kind); + bytes.writeUInt16LE(payload.length, 2); + payload.copy(bytes, 4); + return bytes; +} +const integer = value => { + const bytes = Buffer.alloc(8); + bytes.writeBigUInt64LE(BigInt(value)); + return record(0, bytes); +}; +const string = value => record(1, Buffer.from(`${value}\0`, 'ascii')); +const tuple = children => record(3, Buffer.concat(children)); +const resource = (name, fields) => tuple([string(name), tuple(fields)]); +function fixture(records) { + const payload = Buffer.concat(records); + const header = Buffer.alloc(12); + header.write('AeoB'); + header.writeUInt32LE(12 + payload.length, 4); + header.writeUInt32LE(records.length, 8); + return Buffer.concat([header, payload]); +} +const gpio = values => resource('TLMMGPIO', values.map(integer)); +const vote = (name, values) => resource('PMICVREGVOTE', [string(name), ...values.map(integer)]); +const decodeOne = record => decodeCameraPower(fixture([record])).resources[0]; + +test('six TLMM fields retain raw low/high levels without inventing reset polarity', () => { + const decoded = decodeCameraPower(fixture([gpio([110, 0, 0, 1, 0, 0]), gpio([110, 1, 0, 1, 0, 0])])); + assert.deepEqual(decoded.resources.map(item => item.rawOutputLevel), [0, 1]); + const first = decoded.resources[0]; + assert.equal(first.gpioNumber, 110); + assert.equal(first.outputEnableCode, 1); + assert.equal(first.functionCode, 0); + assert.equal(first.pullCode, 0); + assert.equal(first.equivalentDriveStrengthMilliamp, 2); + assert.equal('activeLow' in first, false); + assert.equal('resetGPIO' in first, false); + assert.equal(first.fields[0].payloadOffset, first.fields[0].offset + 4); +}); + +test('TLMM bitfield boundaries are explicit; unsupported values stay opaque, not masked', () => { + const valid = decodeOne(gpio([1023, 1, 15, 1, 3, 7])); + assert.equal(valid.gpioNumber, 1023); + assert.equal(valid.equivalentDriveStrengthMilliamp, 16); + for (const values of [[1024, 0, 0, 1, 0, 0], [110, 2, 0, 1, 0, 0], + [110, 0, 16, 1, 0, 0], [110, 0, 0, 2, 0, 0], [110, 0, 0, 1, 4, 0], [110, 0, 0, 1, 0, 8]]) { + const unsupported = decodeOne(gpio(values)); + assert.equal(unsupported.interpretation, 'opaque'); + assert.deepEqual(unsupported.raw.children[1].children.map(item => Number(item.value)), values); + } +}); + +test('TLMM extension fields and strings remain opaque rather than introducing inversion', () => { + const decoded = decodeOne(resource('TLMMGPIO', [...[110, 0, 0, 1, 0, 0].map(integer), + string('EGPIO_ENABLE'), integer(0xffffffffffffffffn)])); + assert.equal(decoded.opaqueTail[0].value, 'EGPIO_ENABLE'); + assert.equal(decoded.opaqueTail[1].value, '18446744073709551615'); + assert.equal('activeLow' in decoded, false); +}); + +test('type-one PMIC votes expose only verified voltage, enable and mode fields', () => { + const on = decodeOne(vote('PPP_RESOURCE_ID_LDO6_M', [1, 1800000, 1, 7, 0])); + assert.equal(on.observedInternalVoteType, 0); + assert.equal(on.voltageMicrovolts, 1800000); + assert.equal(on.softwareEnableVote, 1); + assert.equal(on.hardwareModeCode, 7); + assert.equal(on.opaqueFourthVoteField.value, '0'); + assert.equal('supplyName' in on, false); + assert.equal('actuator' in on, false); + const off = decodeOne(vote('../not-opened', [1, 0, 0, 7, 0])); + assert.equal(off.resourceName, '../not-opened'); + assert.equal(off.voltageMicrovolts, 0); + assert.equal(off.softwareEnableVote, 0); +}); + +test('unverified PMIC vote types and non-boolean enable values are not guessed', () => { + const other = decodeOne(resource('PMICVREGVOTE', [string('resource'), integer(12), string('unknown')])); + assert.equal(other.interpretation, 'opaque'); + assert.equal(other.raw.children[1].children[2].value, 'unknown'); + const enable = decodeOne(vote('PPP_RESOURCE_ID_LDO16_B', [1, 2900000, 2, 7, 0])); + assert.equal(enable.interpretation, 'opaque'); + assert.equal('voltageMicrovolts' in enable, false); +}); + +test('PMIC extensions remain opaque and no name is resolved to a file or board consumer', () => { + const item = decodeOne(resource('PMICVREGVOTE', [string('PPP_RESOURCE_ID_LDO16_B'), + ...[1, 2900000, 1, 7, 0].map(integer), string('SUPPRESSIBLE')])); + assert.equal(item.opaqueTail[0].value, 'SUPPRESSIBLE'); + assert.equal('autofocusSupply' in item, false); + assert.equal('client' in item, false); +}); + +test('resource delays are milliseconds, with optional metadata preserved verbatim', () => { + const result = decodeOne(resource('DELAY', [integer(10), integer(123)])); + assert.equal(result.configuredMilliseconds, 10); + assert.equal(result.opaqueTail[0].value, '123'); + assert.equal('microseconds' in result, false); + assert.equal(decodeOne(resource('DELAY', [integer(0)])).configuredMilliseconds, 0); +}); + +test('DEVICE/DSTATE wrappers retain context and configured list order without Linux conversion', () => { + const input = fixture([tuple([string('DEVICE'), integer(133), string('\\_SB.CAMS'), + tuple([string('COMPONENT'), integer(0)]), + tuple([string('DSTATE'), integer(0), gpio([110, 0, 0, 1, 0, 0]), + resource('DELAY', [integer(1)]), gpio([110, 1, 0, 1, 0, 0])]), + tuple([string('DSTATE'), integer(3), gpio([110, 0, 0, 1, 0, 0])])])]); + const result = decodeCameraPower(input); + assert.equal(result.resources[0].interpretation, 'opaque'); + assert.deepEqual(result.resources.slice(1).map(item => item.mechanism), ['TLMMGPIO', 'DELAY', 'TLMMGPIO', 'TLMMGPIO']); + const wrapper = offset => result.wrappers.find(item => item.offset === offset); + assert.equal(wrapper(result.resources[1].contextOffsets[0]).fields[1].value, '\\_SB.CAMS'); + assert.equal(wrapper(result.resources[1].contextOffsets[1]).fields[0].value, '0'); + assert.equal(wrapper(result.resources.at(-1).contextOffsets[1]).fields[0].value, '3'); + assert.ok(result.resources[1].offset < result.resources[2].offset); +}); + +test('wrapper labels are emitted once, not amplified across every child resource', () => { + const label = 'long-inert-device-label-'.repeat(1000); + const records = Array.from({length: 50}, () => resource('DELAY', [integer(1)])); + const input = fixture([tuple([string('DEVICE'), string(label), tuple([string('DSTATE'), integer(0), ...records])])]); + const result = decodeCameraPower(input); + assert.equal(result.resources.length, 50); + assert.equal(result.wrappers.length, 2); + assert.equal(JSON.stringify(result).split(label).length - 1, 1); + assert.ok(result.resources.every(item => item.contextOffsets.every(offset => Number.isSafeInteger(offset)))); + assert.equal(result.wrappers[1].parentOffset, result.wrappers[0].offset); +}); + +test('unknown mechanisms and unlabeled tuples remain opaque including their numeric widths', () => { + const unknown = decodeOne(resource('FUTURE_POWER', [integer(0xffffffffffffffffn)])); + assert.equal(unknown.interpretation, 'opaque'); + assert.equal(unknown.raw.children[1].children[0].value, '18446744073709551615'); + const unlabeled = decodeOne(tuple([integer(7)])); + assert.equal(unlabeled.interpretation, 'opaque'); +}); + +test('known malformed layouts, wrong types and over-wide interpreted fields fail explicitly', () => { + for (const bad of [gpio([110]), resource('TLMMGPIO', [string('110'), ...[0, 0, 1, 0, 0].map(integer)]), + gpio([0x100000000n, 0, 0, 1, 0, 0]), vote('rail', [1, 1800000]), + vote('rail', [1, 0x100000000n, 1, 7, 0]), resource('DELAY', []), + resource('DELAY', [integer(0x100000000n)]), + tuple([string('DEVICE'), integer(1), integer(2)]), + tuple([string('DSTATE'), string('0')]), + tuple([string('TLMMGPIO'), tuple([...[110, 0, 0, 1, 0, 0].map(integer)]), integer(9)])]) { + assert.throws(() => decodeCameraPower(fixture([bad])), error => error.code === 'POWER_FIELDS'); + } +}); + +test('shared structural limits reject hostile size, count and recursion before interpretation', () => { + assert.throws(() => decodeCameraPower(Buffer.alloc(16 * 1024 * 1024 + 1)), error => error.code === 'BYTE_LIMIT'); + const wrongCount = fixture([gpio([110, 0, 0, 1, 0, 0])]); + wrongCount.writeUInt32LE(2, 8); + assert.throws(() => decodeCameraPower(wrongCount), error => error.code === 'COUNT_MISMATCH'); + let deep = integer(0); + for (let i = 0; i < 34; i++) deep = tuple([deep]); + assert.throws(() => decodeCameraPower(fixture([deep])), error => error.code === 'DEPTH_LIMIT'); + assert.throws(() => decodeCameraPower(fixture([])), /power tuples/); + assert.throws(() => decodeCameraPower(fixture([integer(1)])), /power tuples/); +}); + +test('evidence metadata pins the actual driver and instruction-based field observations', () => { + const result = decodeCameraPower(fixture([resource('DELAY', [integer(1)])])); + assert.equal(result.evidence.sha256, POWER_FIELD_EVIDENCE.sha256); + assert.match(result.evidence.tlmm.physicalLevelInstructions, /^0x/); + assert.match(result.sourceSHA256, /^[a-f0-9]{64}$/); + assert.equal(Object.isFrozen(POWER_FIELD_EVIDENCE), true); +}); diff --git a/tools/sp11-driver-audit/ov13858-native.md b/tools/sp11-driver-audit/ov13858-native.md index a960e23b878f4e..d4c8ec904c0602 100644 --- a/tools/sp11-driver-audit/ov13858-native.md +++ b/tools/sp11-driver-audit/ov13858-native.md @@ -44,17 +44,27 @@ silently substitute either sequence for verified board/module requirements. No Denali DTS node is enabled by this series. Resolve and review: -- Linux CCI controller and bus. The platform HAL now establishes raw Windows - address `0x20` as CCI/Linux address `0x10`, but logical master indices still - need matching to the actual firmware resource ordering. -- Reset routing and polarity, supply identities and ordering, including the - separate 2.9 V module/actuator resource. +- Sensor-to-PCFG-slot lookup and current-board confirmation. The platform + HAL establishes raw address `0x20` as CCI/Linux `0x10`; the saved MSHW0495 + allocation plus the HAL consumer now match logical masters 0/1 to CCI0 + and 2/3 to CCI1. This is not a fresh translated-resource hardware trace. +- Reset-net and sensor pin-to-rail ownership. Static analysis establishes + physical GPIO110 low/high levels and the `ldom6`/`ldom1`/`ldom5`/`ldob16` + voltage votes, but does not prove that GPIO110 is reset or that the + separately sequenced 2.9 V rail belongs to an actuator. - Physical CSI lane assignment, link frequency and module-specific mode timing. Windows metadata and programmed frame length currently disagree; output-size agreement is not enough to infer clock timing. - Actuator/OTP calibration ownership and indicator sharing with the front and IR cameras, using native Linux interfaces. +The [resource field notes](camera-fields.md) distinguish configured serial +ordering from the generic driver's asynchronous bulk regulator enable. The +5 ms generic post-reset delay also differs from the module's configured +10 ms. Neither difference is a measured failure or a verified sensor-wide +requirement; do not change generic ACPI behaviour or add a guessed fourth +sensor supply to reproduce an unverified board sequence. + Runtime qualification must cover chip identification, test-pattern/capture layout, start/stop/reopen, fault and probe-deferral paths, indicator release, idle power, system sleep, and existing ACPI users. Source compilation and diff --git a/tools/sp11-driver-audit/windows-resources.mjs b/tools/sp11-driver-audit/windows-resources.mjs new file mode 100644 index 00000000000000..c2e47b603c49f6 --- /dev/null +++ b/tools/sp11-driver-audit/windows-resources.mjs @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Original, offline reader for an explicitly selected observed ARM64 layout. +// Structure definitions, not implementation code, were consulted: +// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_cm_resource_list +// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_cm_partial_resource_descriptor +// +// Evidence note: four identical QCOM0C32/MSHW0495 REG exports constitute ONE +// observation. File SHA256 a46ebcfc358440fc30c196f9dc0c14b028f06a197bd6964bcd93b374b592f061; +// BootConfig == AllocConfig, 200 bytes, resource SHA256 +// f340bd8f69c8e52ce6bfcf82e6912eeea896c0c4a86ba405d5c75815bca9f857. +// Ordinals 2/3 contain 0xac15000/0xac16000, both length 0x1000; these match +// Linux cci0/cci1. IRQ vectors 492/303 additionally match GIC SPI 460/271 +32. +// These are registry allocations, NOT an evaluated _CRS or direct WDF +// translated-resource capture, and do not independently prove sensor routing. +import { createHash } from 'node:crypto'; +import { open } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const WINDOWS_RESOURCE_LIMITS = Object.freeze({ + maxBytes: 1024 * 1024, + maxResources: 4096, + maxValues: 64, + maxLines: 16384, +}); + +export class WindowsResourceDecodeError extends Error { + constructor(code, message, offset = 0) { + super(message); + this.name = 'WindowsResourceDecodeError'; + this.code = code; + this.offset = offset; + } +} + +const digest = (bytes) => createHash('sha256').update(bytes).digest('hex'); +const fail = (code, message, offset = 0) => { + throw new WindowsResourceDecodeError(code, message, offset); +}; + +function limitsFor(options) { + const limits = { ...WINDOWS_RESOURCE_LIMITS }; + for (const [key, value] of Object.entries(options)) { + if (!Object.hasOwn(limits, key)) throw new TypeError(`Unknown decoder option: ${key}`); + if (!Number.isSafeInteger(value) || value < 1 || value > limits[key]) { + throw new RangeError(`${key} must be an integer from 1 to ${limits[key]}`); + } + limits[key] = value; + } + return limits; +} + +function boundedCopy(input, limits) { + if (!(input instanceof Uint8Array)) throw new TypeError('Input must be bytes'); + if (input.byteLength > limits.maxBytes) fail('BYTE_LIMIT', 'Input exceeds the byte limit'); + return Buffer.from(input); +} + +const layoutLimits = [ + 'Caller-selected observed ARM64 layout: 20-byte header and 20-byte partial descriptors; architecture is not autodetected.', + 'Only one full resource list, version 1/revision 1, memory, line-interrupt and connection descriptors are supported.', + 'Unknown resource types, message-signaled interrupts and variable DeviceSpecificData are rejected.', + 'Registry allocation data does not establish whether an address is raw or translated; no _CRS evaluation or WDF trace is performed.', + 'Resource ordinals and connection IDs do not identify sensor routing, Linux GPIOs, I2C channels or hardware policy.', +]; + +/** No MMIO, driver execution, address translation, or automatic Linux mapping. */ +export function decodeCmResourceListArm64(input, options = {}) { + const limits = limitsFor(options); + const bytes = boundedCopy(input, limits); + if (bytes.length < 20) fail('SHORT_HEADER', 'CM_RESOURCE_LIST needs a 20-byte header'); + const fullResourceCount = bytes.readUInt32LE(0); + const version = bytes.readUInt16LE(12); + const revision = bytes.readUInt16LE(14); + const count = bytes.readUInt32LE(16); + if (fullResourceCount !== 1) fail('FULL_LIST_COUNT', 'Only one full resource list is supported'); + if (version !== 1 || revision !== 1) { + fail('UNSUPPORTED_VERSION', 'Only observed version 1/revision 1 is supported', 12); + } + if (count > limits.maxResources) fail('RESOURCE_LIMIT', 'Resource count exceeds the limit', 16); + if (bytes.length !== 20 + count * 20) { + fail('EXTENT_MISMATCH', 'Input extent differs from the selected fixed-descriptor layout', 16); + } + const resources = []; + for (let ordinal = 0; ordinal < count; ordinal++) { + const offset = 20 + ordinal * 20; + const type = bytes[offset]; + const flags = bytes.readUInt16LE(offset + 2); + const resource = { ordinal, offset, type, shareDisposition: bytes[offset + 1], flags }; + if (type === 3) { + Object.assign(resource, { + kind: 'memory', base: `0x${bytes.readBigUInt64LE(offset + 4).toString(16)}`, + length: bytes.readUInt32LE(offset + 12), + opaqueUnionTail: bytes.readUInt32LE(offset + 16), + }); + } else if (type === 2) { + // Only the observed line-interrupt flags 0/1 are interpreted. MSI has a + // different raw union; do not accidentally label its words as vectors. + if (flags & ~1) fail('UNSUPPORTED_INTERRUPT_FLAGS', 'Only line-interrupt flags 0/1 are supported', offset + 2); + Object.assign(resource, { + kind: 'interrupt', levelOrGroupWord: bytes.readUInt32LE(offset + 4), + vector: bytes.readUInt32LE(offset + 8), + affinity: bytes.readBigUInt64LE(offset + 12).toString(10), + }); + } else if (type === 0x84) { + Object.assign(resource, { + kind: 'connection', connectionClass: bytes[offset + 4], + connectionType: bytes[offset + 5], reserved: bytes.readUInt16LE(offset + 6), + connectionId: bytes.readBigUInt64LE(offset + 8).toString(10), + opaqueUnionTail: bytes.readUInt32LE(offset + 16), + }); + } else { + fail('UNSUPPORTED_RESOURCE_TYPE', `Resource type ${type} is outside the supported fixed layout`, offset); + } + resources.push(resource); + } + return { + schema: 'windows-cm-resource-list-observed-arm64-v1', sha256: digest(bytes), bytes: bytes.length, + header: { + fullResourceCount, interfaceTypeRaw: bytes.readUInt32LE(4), busNumber: bytes.readUInt32LE(8), + version, revision, count, + }, + resources, limits: [...layoutLimits], + }; +} + +/** Strict typed hex(8) extraction only, not a general REG importer/evaluator. */ +export function decodeRegistryResources(input, options = {}) { + const limits = limitsFor(options); + const bytes = boundedCopy(input, limits); + if (bytes.length < 2 || bytes[0] !== 0xff || bytes[1] !== 0xfe || bytes.length % 2) { + fail('REG_ENCODING', 'Expected BOM-prefixed UTF-16LE with an even byte extent'); + } + let text; + try { text = new TextDecoder('utf-16le', { fatal: true }).decode(bytes.subarray(2)); } + catch { fail('REG_ENCODING', 'Invalid UTF-16LE registry text'); } + if (text.includes('\0') || /\r(?!\n)/.test(text)) fail('REG_ENCODING', 'Unexpected NUL or bare CR in registry text'); + let lineCount = 1; + for (const ch of text) if (ch === '\n' && ++lineCount > limits.maxLines) { + fail('LINE_LIMIT', 'Registry line count exceeds the limit'); + } + const lines = text.split(/\r?\n/); + if (lines[0] !== 'Windows Registry Editor Version 5.00') fail('REG_HEADER', 'Unsupported registry text header'); + const entries = []; + const identities = new Set(); + let section = null; + let totalResources = 0; + for (let line = 1; line < lines.length; line++) { + const current = lines[line].trim(); + if (!current || current.startsWith(';')) continue; + if (current.startsWith('[')) { + const match = /^\[([^\]\r\n]+)\]$/.exec(current); + if (!match || match[1].startsWith('-')) fail('REG_SECTION', 'Unsupported registry section syntax'); + section = match[1]; + continue; + } + if (!current.startsWith('"') && !current.startsWith('@')) continue; + // Track every recognizable value, including ignored types, to avoid + // presenting an earlier/later hex(8) overwritten by X="..." or hex(2). + // This deliberately rejects escaped names rather than guessing aliases. + const assignment = /^(?:"([^"\\\r\n]{0,256})"|(@))=(.*)$/.exec(current); + if (!assignment || !section) fail('REG_VALUE_SYNTAX', 'Values require a section and an unescaped quoted/default name'); + const name = assignment[2] ? '' : assignment[1]; + const identity = `${section.toLowerCase()}\0${name.toLowerCase()}`; + if (identities.has(identity)) fail('DUPLICATE_VALUE', 'Repeated resource value has ambiguous registry semantics'); + identities.add(identity); + // Unrelated value contents are not decoded or re-emitted. Text mentioning + // hex(8) inside a quoted string is not a typed resource assignment. + const valueText = assignment[3]; + if (!/^hex\(8\)/i.test(valueText)) continue; + const match = /^hex\(8\):(.*)$/i.exec(valueText); + if (!match) fail('REG_VALUE_SYNTAX', 'Typed hex(8) resource value requires a colon'); + if (entries.length >= limits.maxValues) fail('VALUE_LIMIT', 'Resource value count exceeds the limit'); + const parts = []; + let fragment = match[1].trim(); + while (fragment.endsWith('\\')) { + if (!fragment.endsWith(',\\')) fail('REG_CONTINUATION', 'Hex continuation must follow a comma'); + parts.push(fragment.slice(0, -1)); + if (++line >= lines.length) fail('REG_CONTINUATION', 'Unfinished resource value continuation'); + if (!/^[ \t]+/.test(lines[line])) fail('REG_CONTINUATION', 'Continuation must be an indented hex line'); + fragment = lines[line].trim(); + } + parts.push(fragment); + const body = parts.join(''); + if (!/^[ \t]*[0-9a-f]{2}(?:[ \t]*,[ \t]*[0-9a-f]{2})*[ \t]*$/i.test(body)) { + fail('REG_HEX', 'Resource value must contain comma-separated two-digit hex bytes'); + } + const tokens = body.split(','); + // Check extent before allocating the decoded value or constructing nodes. + if (tokens.length > 20 + limits.maxResources * 20) fail('RESOURCE_LIMIT', 'Resource value extent exceeds the limit'); + const payload = Buffer.from(tokens.map((token) => Number.parseInt(token.trim(), 16))); + if (payload.length >= 20 && totalResources + payload.readUInt32LE(16) > limits.maxResources) { + fail('RESOURCE_LIMIT', 'Aggregate registry resource count exceeds the limit', 16); + } + const resourceList = decodeCmResourceListArm64(payload, limits); + totalResources += resourceList.header.count; + entries.push({ section, name, resourceList }); + } + if (!entries.length) fail('NO_RESOURCE_VALUES', 'No typed hex(8) resource values found'); + return { + schema: 'windows-registry-resources-utf16-observed-arm64-v1', + sha256: digest(bytes), bytes: bytes.length, + stats: { values: entries.length, resources: totalResources, distinctResourcePayloads: new Set(entries.map((entry) => entry.resourceList.sha256)).size }, + entries, + limits: [...layoutLimits, 'Only typed hex(8) contents are extracted; unrelated value contents are ignored, and nothing is imported.', + 'All repeated value identities and escaped/ambiguous value names are conservatively rejected, including unrelated value types.', + 'Identical payloads/copies are not independent hardware observations.'], + }; +} + +export async function readWindowsResourceBytes(path) { + const handle = await open(path, 'r'); + try { + const stat = await handle.stat(); + if (!stat.isFile()) throw new TypeError('Input must be a regular file'); + if (stat.size > WINDOWS_RESOURCE_LIMITS.maxBytes) fail('BYTE_LIMIT', 'Input exceeds the byte limit'); + const bytes = Buffer.alloc(stat.size + 1); + let offset = 0; + while (offset < bytes.length) { + const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, null); + if (!bytesRead) break; + offset += bytesRead; + } + if (offset !== stat.size) fail('FILE_CHANGED', 'Input length changed while reading'); + return bytes.subarray(0, offset); + } finally { await handle.close(); } +} + +async function main(args) { + const decoders = { 'registry-utf16-arm64': decodeRegistryResources, 'cm-arm64': decodeCmResourceListArm64 }; + if (args.length !== 4 || args[0] !== '--format' || !Object.hasOwn(decoders, args[1]) || args[2] !== '--input' || !args[3]) { + throw new TypeError('Usage: node windows-resources.mjs --format registry-utf16-arm64|cm-arm64 --input FILE'); + } + process.stdout.write(`${JSON.stringify(decoders[args[1]](await readWindowsResourceBytes(args[3])), null, 2)}\n`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + main(process.argv.slice(2)).catch((error) => { + process.stderr.write(`${JSON.stringify({ error: error.code ?? error.name, message: error.message, + ...(error instanceof WindowsResourceDecodeError ? { offset: error.offset } : {}) })}\n`); + process.exitCode = 1; + }); +} diff --git a/tools/sp11-driver-audit/windows-resources.test.mjs b/tools/sp11-driver-audit/windows-resources.test.mjs new file mode 100644 index 00000000000000..e1c66988bb099c --- /dev/null +++ b/tools/sp11-driver-audit/windows-resources.test.mjs @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0-only +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { WINDOWS_RESOURCE_LIMITS, WindowsResourceDecodeError, decodeCmResourceListArm64, decodeRegistryResources } from './windows-resources.mjs'; + +// All bytes are independently generated synthetic fixtures, not OEM captures. +const hash = (bytes) => createHash('sha256').update(bytes).digest('hex'); +function descriptor(type, flags = 0) { + const bytes = Buffer.alloc(20); + bytes[0] = type; + bytes[1] = 1; + bytes.writeUInt16LE(flags, 2); + return bytes; +} +function memory(base = 0x123456789abcdef0n, length = 0x2000) { + const bytes = descriptor(3); + bytes.writeBigUInt64LE(base, 4); + bytes.writeUInt32LE(length, 12); + bytes.writeUInt32LE(0xabcdef01, 16); + return bytes; +} +function interrupt(flags = 1) { + const bytes = descriptor(2, flags); + bytes.writeUInt32LE(0x10203, 4); + bytes.writeUInt32LE(711, 8); + bytes.writeBigUInt64LE(0xffffffffffffffffn, 12); + return bytes; +} +function connection() { + const bytes = descriptor(0x84); + bytes[4] = 1; + bytes[5] = 2; + bytes.writeUInt16LE(0xabcd, 6); + bytes.writeBigUInt64LE(0xfedcba9876543210n, 8); + bytes.writeUInt32LE(0x76543210, 16); + return bytes; +} +function resourceList(resources = []) { + const header = Buffer.alloc(20); + header.writeUInt32LE(1, 0); + header.writeUInt32LE(15, 4); + header.writeUInt32LE(7, 8); + header.writeUInt16LE(1, 12); + header.writeUInt16LE(1, 14); + header.writeUInt32LE(resources.length, 16); + return Buffer.concat([header, ...resources]); +} +const hex = (bytes) => [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join(','); +function registry(body, eol = '\r\n') { + return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(['Windows Registry Editor Version 5.00', '', ...body].join(eol), 'utf16le')]); +} +function rejects(code, fn) { + assert.throws(fn, (error) => { + assert.ok(error instanceof WindowsResourceDecodeError); + assert.equal(error.code, code); + assert.ok(Number.isInteger(error.offset)); + return true; + }); +} + +test('observed ARM64 structure preserves ordinals, offsets, full-width fields and opaque tails', () => { + const bytes = resourceList([memory(), interrupt(), connection()]); + const result = decodeCmResourceListArm64(new Uint8Array(bytes)); + assert.equal(result.sha256, hash(bytes)); + assert.deepEqual(result.header, { fullResourceCount: 1, interfaceTypeRaw: 15, busNumber: 7, version: 1, revision: 1, count: 3 }); + assert.deepEqual(result.resources.map((resource) => [resource.ordinal, resource.offset, resource.kind]), [[0, 20, 'memory'], [1, 40, 'interrupt'], [2, 60, 'connection']]); + assert.equal(result.resources[0].base, '0x123456789abcdef0'); + assert.equal(result.resources[0].length, 0x2000); + assert.equal(result.resources[0].opaqueUnionTail, 0xabcdef01); + assert.equal(result.resources[1].levelOrGroupWord, 0x10203); + assert.equal(result.resources[1].vector, 711); + assert.equal(result.resources[1].affinity, '18446744073709551615'); + assert.equal(result.resources[2].connectionId, '18364758544493064720'); + assert.equal(result.resources[2].reserved, 0xabcd); + assert.equal(result.resources[2].opaqueUnionTail, 0x76543210); + assert.deepEqual(decodeCmResourceListArm64(resourceList()).resources, []); +}); + +test('strict counts, exact extent and version reject truncation, alternate packing and trailing data', () => { + rejects('SHORT_HEADER', () => decodeCmResourceListArm64(Buffer.alloc(19))); + for (const count of [0, 2]) { + const bytes = resourceList(); bytes.writeUInt32LE(count, 0); + rejects('FULL_LIST_COUNT', () => decodeCmResourceListArm64(bytes)); + } + for (const offset of [12, 14]) { + const bytes = resourceList(); bytes.writeUInt16LE(2, offset); + rejects('UNSUPPORTED_VERSION', () => decodeCmResourceListArm64(bytes)); + } + const bytes = resourceList([memory()]); + rejects('EXTENT_MISMATCH', () => decodeCmResourceListArm64(bytes.subarray(0, -1))); + rejects('EXTENT_MISMATCH', () => decodeCmResourceListArm64(Buffer.concat([bytes, Buffer.alloc(1)]))); + rejects('EXTENT_MISMATCH', () => decodeCmResourceListArm64(Buffer.concat([bytes.subarray(0, 20), Buffer.alloc(16)]))); + const tooMany = resourceList(); tooMany.writeUInt32LE(0xffffffff, 16); + rejects('RESOURCE_LIMIT', () => decodeCmResourceListArm64(tooMany)); +}); + +test('unknown, variable-length and MSI resource interpretations fail closed', () => { + for (const type of [0, 1, 5, 7, 0x80, 0xff]) { + rejects('UNSUPPORTED_RESOURCE_TYPE', () => decodeCmResourceListArm64(resourceList([descriptor(type)]))); + } + for (const flags of [2, 3, 0x20, 0x8000]) { + rejects('UNSUPPORTED_INTERRUPT_FLAGS', () => decodeCmResourceListArm64(resourceList([interrupt(flags)]))); + } + assert.equal(decodeCmResourceListArm64(resourceList([interrupt(0)])).resources[0].flags, 0); +}); + +test('typed UTF-16 REG extraction handles continuations and counts identical payloads once', () => { + const bytes = resourceList([memory(), interrupt()]); + const tokens = hex(bytes).split(','); + const input = registry(['[HKEY_LOCAL_MACHINE\\SYNTHETIC\\Device]', '"Ignored"=hex(2):not-read', + '"Literal"="text containing =hex(8):not a resource"', + `"BootConfig"=hex(8):${tokens.slice(0, 23).join(',')},\\`, + ` ${tokens.slice(23).join(',')}`, `"AllocConfig"=hex(8):${hex(bytes)}`, '; synthetic comment']); + const result = decodeRegistryResources(input); + assert.equal(result.sha256, hash(input)); + assert.deepEqual(result.stats, { values: 2, resources: 4, distinctResourcePayloads: 1 }); + assert.deepEqual(result.entries.map((entry) => entry.name), ['BootConfig', 'AllocConfig']); + assert.equal(result.entries[0].section, 'HKEY_LOCAL_MACHINE\\SYNTHETIC\\Device'); + assert.equal(result.entries[0].resourceList.sha256, hash(bytes)); + assert.equal(decodeRegistryResources(registry(['[Synthetic]', `"X"=HEX(8):${hex(bytes)}`], '\n')).entries.length, 1); +}); + +test('REG encoding is UTF-16LE only, fatal for invalid surrogates, NUL, odd extent and wrong header', () => { + const good = registry(['[Synthetic]', `"X"=hex(8):${hex(resourceList())}`]); + rejects('REG_ENCODING', () => decodeRegistryResources(good.subarray(2))); + rejects('REG_ENCODING', () => decodeRegistryResources(good.subarray(0, -1))); + rejects('REG_ENCODING', () => decodeRegistryResources(Buffer.from([0xfe, 0xff, 0, 0]))); + rejects('REG_ENCODING', () => decodeRegistryResources(Buffer.from([0xff, 0xfe, 0, 0xd8]))); + rejects('REG_ENCODING', () => decodeRegistryResources(registry(['\0']))); + rejects('REG_ENCODING', () => decodeRegistryResources(registry(['bare\rreturn']))); + const wrongHeader = Buffer.from(good); wrongHeader.writeUInt16LE(0x58, 2); + rejects('REG_HEADER', () => decodeRegistryResources(wrongHeader)); +}); + +test('malformed hex, continuation and duplicate effective registry values are rejected', () => { + for (const body of ['', '0', '000', '00,', ',00', '00,,00', '0 1', 'zz', '00,0g']) { + rejects('REG_HEX', () => decodeRegistryResources(registry(['[Synthetic]', `"X"=hex(8):${body}`]))); + } + rejects('REG_CONTINUATION', () => decodeRegistryResources(registry(['[Synthetic]', '"X"=hex(8):00,\\']))); + rejects('REG_CONTINUATION', () => decodeRegistryResources(registry(['[Synthetic]', '"X"=hex(8):00,\\', '[Other]']))); + rejects('REG_CONTINUATION', () => decodeRegistryResources(registry(['[Synthetic]', '"X"=hex(8):0\\', ' 1']))); + rejects('REG_HEX', () => decodeRegistryResources(registry(['[Synthetic]', '"X"=hex(8):00,\\', ' "NotHex"=1']))); + rejects('REG_VALUE_SYNTAX', () => decodeRegistryResources(registry([`"X"=hex(8):${hex(resourceList())}`]))); + rejects('REG_VALUE_SYNTAX', () => decodeRegistryResources(registry(['[Synthetic]', '"Escaped\\Name"=hex(8):00']))); + rejects('REG_SECTION', () => decodeRegistryResources(registry(['[-Synthetic]']))); + rejects('REG_SECTION', () => decodeRegistryResources(registry(['[Unclosed']))); + rejects('NO_RESOURCE_VALUES', () => decodeRegistryResources(registry(['[Synthetic]', '"X"="ignored"']))); + const payload = hex(resourceList()); + rejects('DUPLICATE_VALUE', () => decodeRegistryResources(registry(['[Synthetic]', `"BootConfig"=hex(8):${payload}`, '[sYNTHETIC]', `"bootconfig"=hex(8):${payload}`]))); + assert.equal(decodeRegistryResources(registry(['[A]', `"X"=hex(8):${payload}`, '[B]', `"X"=hex(8):${payload}`])).entries.length, 2); +}); + +test('ignored value types cannot silently override a resource in either order', () => { + const typed = `"X"=hex(8):${hex(resourceList([memory()]))}`; + for (const unrelated of ['"x"="string"', '"x"=hex(2):00,00', '"x"=-']) { + for (const assignments of [[typed, unrelated], [unrelated, typed]]) { + rejects('DUPLICATE_VALUE', () => decodeRegistryResources(registry(['[Synthetic]', ...assignments]))); + } + } + rejects('DUPLICATE_VALUE', () => decodeRegistryResources(registry(['[Synthetic]', '@="default"', `""=hex(8):${hex(resourceList())}`]))); + for (const escaped of ['"\\X"="ignored"', '"X\\\\"="ignored"', '"X\\"Y"="ignored"']) { + for (const assignments of [[typed, escaped], [escaped, typed]]) { + rejects('REG_VALUE_SYNTAX', () => decodeRegistryResources(registry(['[Synthetic]', ...assignments]))); + } + } +}); + +test('bounds include aggregate descriptors across repeated lists and overrides only lower limits', () => { + const bytes = resourceList([memory(), memory()]); + rejects('RESOURCE_LIMIT', () => decodeCmResourceListArm64(bytes, { maxResources: 1 })); + rejects('BYTE_LIMIT', () => decodeCmResourceListArm64(bytes, { maxBytes: bytes.length - 1 })); + const input = registry(['[Synthetic]', `"A"=hex(8):${hex(bytes)}`, `"B"=hex(8):${hex(bytes)}`]); + rejects('RESOURCE_LIMIT', () => decodeRegistryResources(input, { maxResources: 3 })); + rejects('VALUE_LIMIT', () => decodeRegistryResources(input, { maxValues: 1 })); + rejects('LINE_LIMIT', () => decodeRegistryResources(input, { maxLines: 2 })); + rejects('BYTE_LIMIT', () => decodeRegistryResources(input, { maxBytes: input.length - 1 })); + for (const [name, maximum] of Object.entries(WINDOWS_RESOURCE_LIMITS)) { + for (const value of [0, -1, 1.1, maximum + 1, NaN]) { + assert.throws(() => decodeCmResourceListArm64(bytes, { [name]: value }), RangeError); + } + } + assert.throws(() => decodeRegistryResources(input, { unknown: 1 }), TypeError); + assert.throws(() => decodeCmResourceListArm64('not bytes'), TypeError); +}); + +test('CLI explicitly selects its layout, reads only, and reports machine-readable failures', async () => { + const dir = await mkdtemp(join(tmpdir(), 'windows-resources-test-')); + const cli = fileURLToPath(new URL('./windows-resources.mjs', import.meta.url)); + try { + const file = join(dir, 'synthetic.reg'); + await writeFile(file, registry(['[Synthetic]', `"X"=hex(8):${hex(resourceList([connection()]))}`])); + const result = spawnSync(process.execPath, [cli, '--format', 'registry-utf16-arm64', '--input', file], { encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); + assert.equal(JSON.parse(result.stdout).entries[0].resourceList.resources[0].kind, 'connection'); + const missingFormat = spawnSync(process.execPath, [cli, '--input', file], { encoding: 'utf8' }); + assert.notEqual(missingFormat.status, 0); + assert.equal(JSON.parse(missingFormat.stderr).error, 'TypeError'); + const binary = join(dir, 'synthetic.bin'); + await writeFile(binary, resourceList([memory()])); + const direct = spawnSync(process.execPath, [cli, '--format', 'cm-arm64', '--input', binary], { encoding: 'utf8' }); + assert.equal(direct.status, 0, direct.stderr); + assert.equal(JSON.parse(direct.stdout).resources[0].kind, 'memory'); + await writeFile(binary, Buffer.alloc(2)); + const invalid = spawnSync(process.execPath, [cli, '--format', 'cm-arm64', '--input', binary], { encoding: 'utf8' }); + assert.notEqual(invalid.status, 0); + assert.equal(JSON.parse(invalid.stderr).error, 'SHORT_HEADER'); + const directory = spawnSync(process.execPath, [cli, '--format', 'cm-arm64', '--input', dir], { encoding: 'utf8' }); + assert.notEqual(directory.status, 0); + assert.equal(JSON.parse(directory.stderr).error, 'TypeError'); + } finally { await rm(dir, { recursive: true }); } +}); From 7a9e3faf68f4bac8df0ab92ba5fd97f881e3fc08 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 12:30:09 +0100 Subject: [PATCH 05/17] tools: record camera route proof and native source workflow Document the actual rear, front and auxiliary PCFG consumers, including the auxiliary address distinction. Keep requested PHY timing separate from sensor PLL and exposure clocks, and retain board, mode and illumination qualification gates. Describe source-only OV13858 and VD55G0 foundations without introducing Windows production dependencies or hardware enablement. Signed-off-by: Leon Silcott --- tools/sp11-driver-audit/README.md | 8 +- tools/sp11-driver-audit/camera-fields.md | 84 ++++++++++++++- .../native-source-bundles.md | 100 ++++++++++++++++++ tools/sp11-driver-audit/ov13858-native.md | 16 +-- 4 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 tools/sp11-driver-audit/native-source-bundles.md diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index c5ea37fa42584f..12c0536745a0ab 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -118,7 +118,7 @@ registers. Neither check establishes CSI timing or successful rear capture. | --- | --- | --- | | Front IMX681 | Native driver and OLED graph; prior preview and indicator tests | Compare verified power, lane and stream/error lifecycle with this package | | Rear OV13858 | Native OF power-management foundation under development; no Denali graph | Verified board resources, module-specific PHY/modes, actuator and indicator ownership | -| IR VD55G0/VD55G1 | Different sensors and board variants; support differs | Exact module identity, native driver, transport and illumination limits | +| IR VD55G0/VD55G1 | Native family-driver port isolated from the tested carrier; G0 illumination rejected | Actual module identity, board transport, sensor-only capture and separate illumination qualification | | Touch/pen | Native transport and userspace exist; fresh-boot autoload repair is a separate series | Preserve repair and investigate actual lifecycle gaps | | Suspend | Several device and SoC resource owners are involved | Decode power policies and compare failed Linux transitions with measured Windows behaviour | | Sensors | Surface SAM support is distinct from the Qualcomm sensor hub | Transport, exact board configuration, orientation and calibration | @@ -134,6 +134,12 @@ tools. It uses Linux resource interfaces, leaves existing sensor tables intact and does not load the inspected configuration files. Its board-enablement and hardware-qualification gates remain explicit. +The [source-only camera bundle guide](native-source-bundles.md) describes +the independent OV13858 and VD55G0 patch sets, their native upstream/vendor +provenance and the source/object-check workflow. They are foundations for +qualification, not installable camera fixes. In particular, a native IR +sensor driver is not equivalent to safely enabling an illuminator. + The research checkout began at the older integration revision `2cbd1ec3e2da385e7bd91fd65c63ba5a8fb5b865`. These tools are independent of that kernel revision and are fork research infrastructure. Hardware changes must diff --git a/tools/sp11-driver-audit/camera-fields.md b/tools/sp11-driver-audit/camera-fields.md index eb80f0315886c5..7afda03a89465d 100644 --- a/tools/sp11-driver-audit/camera-fields.md +++ b/tools/sp11-driver-audit/camera-fields.md @@ -56,8 +56,8 @@ field-specific log arguments support these names: Flag bit 8 indicates position-offset data and bit 9 indicates privacy-shutter data. Tail words remain uninterpreted in the source tool. Orientation values -are not degrees, Windows I2C indices are not a proven Linux bus mapping, and -neither CSI lane layout nor link frequency follows from these fields. +are not degrees. An I2C index alone does not identify a Linux bus, and neither +CSI lane layout nor link frequency follows from these fields. The MSHW0495 bank-zero words begin `0x00110000`, `0x00230010`, `0x01000110`. The second word's CSIPHY 2 agrees with the existing front-camera graph. The @@ -144,8 +144,7 @@ consumer and index split gives this controller map: These are captured registry allocations, not an evaluated `_CRS` or a direct WDF translated-list dump. Do not present them as either, infer physical sensor presence from them, or assume a different board revision has identical wiring. -Associating a PCFG slot with a sensor still requires the sensor-ID lookup -chain; controller mapping alone is not board enablement. +Controller mapping alone is not board enablement. The bounded reader follows the documented [resource-list structure](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_cm_resource_list) @@ -153,6 +152,83 @@ and [partial-descriptor fields](https://learn.microsoft.com/en-us/windows-hardwa with an explicit observed-layout selection. It never applies resources or turns connection IDs into Linux GPIO numbers. +### Sensor slots and configured routes + +Separate instruction-level inspection closes the PCFG consumer association; +this is not inferred from privacy-indicator labels. Each sensor getter asks +the platform for a `0x388`-byte table with command `0x232083`. Rear getter +`0x140005188` copies the first slot at table offset `0x24`. Front getter +`0x140001b48` copies offset `0x90` and records index one; auxiliary getter +`0x140001300` copies offset `0xfc` and records index two. These offsets match +the parser's `0x6c`-byte slot stride. The inspected auxiliary driver SHA-256 is +`e5b6b064f39cf239ab07e22ca2434e3c08c93b691dc7d27cebb90861226efa75`. + +Combining the MSHW0495 bank-zero values, those consumers, the controller map +and each SCFG address gives these configured routes: + +| Sensor consumer | PCFG slot | Logical master | Linux bus/address | CSIPHY index | +| --- | --- | --- | --- | --- | +| Rear | 0 | 1 | `cci0_i2c1`, `0x10` | 1 | +| Front | 1 | 3 | `cci1_i2c1`, `0x10` | 2 | +| Auxiliary | 2 | 0 | `cci0_i2c0`, `0x60` | 0 | + +The auxiliary address is not the front/rear address. Its SCFG file, SHA-256 +`1c848ac4ddbe12948cae4affee421e3a457cac61f98dacb30043841bf0bd1bff`, +contains raw `0xc0`, with expected probe bytes `30 47` at register zero. +That is consistent with the first two little-endian bytes of native VD55G0 +ID `0x53354730`, but not a substitute for a physical full-ID/revision check. +The alternate PCFG bank is zero in this file; the effective bank and actual +board revision still need confirmation. No graph, rail ownership or sensor +presence is established merely by publishing these configured routes. + +## Rear mode and PHY request + +The rear module's serialized mode fields were traced through the actual +deserializer and request producer in `QcDeviceMFT8380.dll`, SHA-256 +`c241b7fbb2ec54e439752a1ea7ad25da10ca740012a54bd0e7a87ea94a141c35`. +The chain from `0x180881218` through `0x18087eab0` and `0x18087d020` +expands each 252-byte serialized mode into a `0x248`-byte runtime record. +Request producer `0x18071f308` consumes the following proven mappings: + +| Field | Serialized mode offset | Runtime offset | +| --- | --- | --- | +| Line length | `0x24` | `0x44` | +| Frame length | `0x28` | `0x48` | +| Output pixel clock | `0x34` | `0x54` | +| Maximum frame rate | `0x40` | `0x60` | +| Lane count | `0x50` | `0x74` | +| Requested settle time | `0x5c` | `0x84` | +| D-PHY/C-PHY phase | `0x60` | `0x88` | + +All three inspected rear modes describe four-lane D-PHY, RAW10, 100 ns +requested settle time and 474,240,000 output pixels/s. The producer applies +a runtime percentage overhead before calculating per-lane bit rate. Only +at zero overhead would the nominal D-PHY request be 592,800,000 Hz: +`474240000 * 10 / 4 / 2`. The effective overhead is unresolved. This is a +metadata-derived receiver request, not verified sensor PLL timing or a new +Linux link-frequency mode. + +The published `vtPixelClock` is synthesized from line length, frame length +and maximum FPS in this path. It is not the output pixel clock. The listed +frame length is 3214, while the last listed sensor writes encode 3208; +dynamic frame-duration/exposure programming remains untraced. Do not derive +Linux exposure/blanking controls by conflating these clock domains. + +`qccammipicsi8380.sys`, SHA-256 +`033f5b1431ad4c76a12ac3b7f0a5be42e460a03bcff40d249511b3034786d407`, +separates D-PHY from C-PHY and consumes the bit-rate and nanosecond fields. +The configured `laneAssign = 0x3210` is unpacked as `0,1,2,3`. Native +instructions show that forwarding even though decompiler output omits its +local array; decompiled indirect-call arguments alone were insufficient. + +The four-lane Windows PHY mask `0xd5` agrees with Linux's X1E PHY table. +Linux's gen2 enable path produces that mask from receiver data positions +`0,1,2,3`, copying the receiver endpoint positions directly. Sensor-local +`1,2,3,4` numbering and clock lane zero are a different endpoint contract: +do not blindly copy them onto the CAMSS receiver or reinterpret the fixed +hardware clock-enable bit as a DT clock-lane number. Physical lane mapping, +polarities, sensor PLL and runtime capture still require qualification. + ## Power-resource delays Static analysis of `qcpep8380.sys`, SHA-256 diff --git a/tools/sp11-driver-audit/native-source-bundles.md b/tools/sp11-driver-audit/native-source-bundles.md new file mode 100644 index 00000000000000..fd6df676aafc1b --- /dev/null +++ b/tools/sp11-driver-audit/native-source-bundles.md @@ -0,0 +1,100 @@ +# Native camera source foundations + +These source-only foundations are development material, not a qualified +kernel release or a ready-to-install camera fix. They do not enable rear or +IR camera nodes on Denali, change a boot default, or replace the working +front-camera, audio or touch configuration. + +The production code uses native Linux I2C/CCI, V4L2, firmware-node, regulator, +clock and GPIO interfaces. Building it does not require Windows, the MSI, +the research readers or any Windows driver/configuration file. The VD55G0 +driver includes sensor firmware patches published in ST's GPL-2.0 Linux +reference driver; Windows independence does not mean firmware independence. +This does not resolve firmware sourcing for every other SP11 subsystem. + +## Contents and provenance + +- `patches/ov13858/`: the OV13858 binding and native OF power-management + commits, with existing sensor register tables retained. +- `patches/vd55g0/`: the native VD55G1-family prerequisite/VD55G0 series and + separate local corrections. Original contributors' authorship and sign-offs + are retained; the port does not import the newer media-core pad API or + unrelated Intel IPU/x86 platform changes. +- `source/`: exact driver and binding snapshots for review. They belong in a + full kernel tree, not a stand-alone Windows compatibility module or DKMS + shim. +- `MANIFEST.json` and `SHA256SUMS`: exact carrier, source revisions, source + hashes and patch inventory. The manifest distinguishes source checks from + physical qualification. Licence texts accompany the source. + +The carrier is the tested v8 source revision +`8ba2b75713feb5c72bbc11dd8cce4048e23cdeea` from +[the kernel fork](https://github.com/ooaklee/linux_ms_dev_kit-sp11). +That does not make these new changes hardware-tested. Later carrier updates, +including the separate touch-autoload repair, require their own integration +and regression checks. Fork research tools and CI are not part of the native +driver patch sets. + +VD55G0 source is based on +[Peter Marshall's native series](https://github.com/petm5/linux/commit/4a04accd5f688ac4fdca1ad4d3959ef17825085c), +the existing ST driver, and +[Jizhou Tong's register-width correction](https://patchwork.linuxtv.org/project/linux-media/patch/010601a1332f51fe-f5c0bb2d-cf7f-4e99-9feb-1e8f65057945-000000@ap-northeast-1.amazonses.com/). +The two embedded VD55G0 firmware payloads were compared byte-for-byte with +[ST's pinned Linux reference](https://github.com/STMicroelectronics/vd55g0-linux-driver/blob/a05627b0f6d8775aa54b6fa306e91f425f2cbf9e/vd55g0_patches.h). + +## Apply and compile for source review + +Use a separate full kernel checkout at the manifest's carrier revision. +Verify `SHA256SUMS` inside the extracted bundle before applying either +independent patch set. From the kernel checkout: + +```sh +git switch -c sp11-native-camera-research +git am /path/to/source-bundle/patches/ov13858/*.patch +git am /path/to/source-bundle/patches/vd55g0/*.patch +``` + +For an ARM64 Linux source/object check, with the normal kernel build +dependencies installed: + +```sh +mkdir -p ../sp11-native-camera-build +python3 debian/scripts/misc/annotations \ + --file debian.qcom-x1e/config/annotations \ + --arch arm64 --flavour qcom-x1e --export \ + > ../sp11-native-camera-build/.config +scripts/config --file ../sp11-native-camera-build/.config \ + --module VIDEO_OV13858 --module VIDEO_VD55G1 +make O=../sp11-native-camera-build ARCH=arm64 olddefconfig +make O=../sp11-native-camera-build ARCH=arm64 W=1 -j4 \ + drivers/media/i2c/ov13858.o drivers/media/i2c/vd55g1.o +``` + +`olddefconfig` adapts to the available compiler/tools; review its configuration +delta rather than calling it an identical release configuration. Cross builds +also require an appropriate `CROSS_COMPILE`. Driver-object compilation does +not link a full kernel/module, produce an installable package, or validate +CSI capture. Do not use the resulting objects with `insmod`. + +## Deliberate qualification gates + +OV13858 still needs verified reset-net and sensor pin-to-rail ownership, +module/actuator calibration handling, sensor/receiver lane mapping and +timing, followed by capture/lifecycle tests. See +[the OV13858 qualification notes](ov13858-native.md). + +VD55G0 illumination is deliberately unsupported in this foundation: +`st,leds` is rejected for that sensor before resources are powered. The +driver does not guess GPIO1 wiring or manufacture a flash control. Sensor +GPIOs are configured as inputs after firmware reaches standby and before +streaming. This is a software policy, not proof of undocumented reset +defaults, absence of emitted IR, or electrical/eye safety. + +IR board identity, actual I2C routing/address, supplies/reset, CSI graph, +capture timing, calibration and sensor-only runtime behaviour remain to be +qualified. Illuminator wiring, first-frame and live-control exposure limits, +current control and independent safety requirements need a separate review +before illumination support can be added. Full capture, module linkage, +failure recovery, suspend/resume and existing VD55G1/G4 regressions remain +unproven. No device installation instructions are supplied for this +unqualified foundation. diff --git a/tools/sp11-driver-audit/ov13858-native.md b/tools/sp11-driver-audit/ov13858-native.md index d4c8ec904c0602..70a3725aca77ff 100644 --- a/tools/sp11-driver-audit/ov13858-native.md +++ b/tools/sp11-driver-audit/ov13858-native.md @@ -44,17 +44,19 @@ silently substitute either sequence for verified board/module requirements. No Denali DTS node is enabled by this series. Resolve and review: -- Sensor-to-PCFG-slot lookup and current-board confirmation. The platform - HAL establishes raw address `0x20` as CCI/Linux `0x10`; the saved MSHW0495 - allocation plus the HAL consumer now match logical masters 0/1 to CCI0 - and 2/3 to CCI1. This is not a fresh translated-resource hardware trace. +- Current-board and effective-PCFG-bank confirmation. Static consumer + tracing now associates the rear with slot zero; the saved MSHW0495 + allocation, HAL and SCFG together describe `cci0_i2c1` at `0x10`, CSIPHY1. + This is not a fresh translated-resource or physical capture trace. - Reset-net and sensor pin-to-rail ownership. Static analysis establishes physical GPIO110 low/high levels and the `ldom6`/`ldom1`/`ldom5`/`ldob16` voltage votes, but does not prove that GPIO110 is reset or that the separately sequenced 2.9 V rail belongs to an actuator. -- Physical CSI lane assignment, link frequency and module-specific mode - timing. Windows metadata and programmed frame length currently disagree; - output-size agreement is not enough to infer clock timing. +- Physical sensor-to-receiver lane assignment, polarities, sensor PLL and + module-specific timing. The verified Windows producer's nominal 592.8 MHz + request assumes zero overhead, whose effective setting is unresolved; + it is not a new validated Linux mode. Metadata and listed frame-length + writes still disagree, and output and exposure clocks are distinct. - Actuator/OTP calibration ownership and indicator sharing with the front and IR cameras, using native Linux interfaces. From 3a594732500a831591ccac7ac8c384a9fe18a58d Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 12:34:29 +0100 Subject: [PATCH 06/17] tools: distinguish mode-field consumers from the PHY producer Signed-off-by: Leon Silcott --- tools/sp11-driver-audit/camera-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sp11-driver-audit/camera-fields.md b/tools/sp11-driver-audit/camera-fields.md index 7afda03a89465d..1a3dfc0a106e46 100644 --- a/tools/sp11-driver-audit/camera-fields.md +++ b/tools/sp11-driver-audit/camera-fields.md @@ -188,7 +188,7 @@ deserializer and request producer in `QcDeviceMFT8380.dll`, SHA-256 `c241b7fbb2ec54e439752a1ea7ad25da10ca740012a54bd0e7a87ea94a141c35`. The chain from `0x180881218` through `0x18087eab0` and `0x18087d020` expands each 252-byte serialized mode into a `0x248`-byte runtime record. -Request producer `0x18071f308` consumes the following proven mappings: +The deserializer and downstream consumers use the following proven mappings: | Field | Serialized mode offset | Runtime offset | | --- | --- | --- | From b3066dc9e356f340b51c3c9ce42ee8106757ba73 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 14:59:43 +0100 Subject: [PATCH 07/17] tools: record native camera revision-two validation Document the focused HDR exposure-limit correction and the replay of all 24 native camera patches against the exact tested carrier tree. Preserve the distinction between the previous full kernel/module build and the corrected driver's focused build and synthetic regression tests. Record the source and artifact identities, retained contribution history, and remaining board, runtime and illumination qualification boundaries. Signed-off-by: Leon Silcott --- tools/sp11-driver-audit/README.md | 6 +- .../native-camera-validation.md | 82 +++++++++++++++++++ .../native-source-bundles.md | 48 +++++++++-- tools/sp11-driver-audit/ov13858-native.md | 10 ++- 4 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 tools/sp11-driver-audit/native-camera-validation.md diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index 12c0536745a0ab..26ad62278e0848 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -136,9 +136,9 @@ hardware-qualification gates remain explicit. The [source-only camera bundle guide](native-source-bundles.md) describes the independent OV13858 and VD55G0 patch sets, their native upstream/vendor -provenance and the source/object-check workflow. They are foundations for -qualification, not installable camera fixes. In particular, a native IR -sensor driver is not equivalent to safely enabling an illuminator. +provenance and the source, object and module validation workflow. They are +foundations for qualification, not installable camera fixes. In particular, a +native IR sensor driver is not equivalent to safely enabling an illuminator. The research checkout began at the older integration revision `2cbd1ec3e2da385e7bd91fd65c63ba5a8fb5b865`. These tools are independent of that diff --git a/tools/sp11-driver-audit/native-camera-validation.md b/tools/sp11-driver-audit/native-camera-validation.md new file mode 100644 index 00000000000000..01982cfdc49e50 --- /dev/null +++ b/tools/sp11-driver-audit/native-camera-validation.md @@ -0,0 +1,82 @@ +# Native camera validation scope + +These source-only foundations use the tested-v8 carrier +`8ba2b75713feb5c72bbc11dd8cce4048e23cdeea`. The source bundles include no Debian +packages, boot images, Windows payloads, EFI applications, private captures or +signing keys. No board nodes or shipping configuration are enabled. + +## Previous revision: complete kernel and module link + +The previous bundle, SHA-256 +`ec319fe60543a555c2a34212477ade671ef23765d43e0f8addd526cb3d8fc405`, +contained 2 OV13858 and 21 IR patches. Its IR source ended at +`14bb8959fc37812a23ed55af73a2eb273acdb043`, driver SHA-256 +`ee61374030f80665fbbba0497b46010f222c2f97118505be4909d8ffa60bdc5c`. + +- A native ARM64 GCC 13.3.0/binutils 2.42 build completed with + `make ARCH=arm64 W=1 -j4 vmlinux modules`. +- `vmlinux` and all 7,817 modules listed in `modules.order` were present and + identified as little-endian AArch64 ELF files. The modified drivers had no + compiler warnings or errors. There were 1,308 compiler-warning lines + elsewhere in the carrier; this was not a warning-free whole-kernel build. +- The effective configuration was unchanged by `olddefconfig`, SHA-256 + `bccba597adeb6475571f813c278ca02e6a478c2134f862cf64e1319ada10bdf7`. + It enabled both sensor drivers as modules for validation and disabled debug + information; it was not an identical release configuration/toolchain. +- Its `vmlinux` SHA-256 was + `83a4abe6b3a00e17c270c6820d88c99e441b1fe196816246cfc656106bf8d31c`. + +That complete build did **not** contain the HDR correction added in revision 2. + +## Current revision: HDR correction + +Revision 2 retains all 23 earlier patch emails byte-for-byte and appends IR +commit `5c961dcbcb4e7742124e402800317f693634ddb4`, for 24 patches total. +The current IR driver SHA-256 is +`086ec2feff305a159d93a283c0e303fd04108c798e6c4252d34bfe5e45c9b5b1`. +The other three driver/binding snapshots are unchanged. + +An independent replay applied all 24 unmodified emails with `git am` to the +complete original v8 commit and tree on a fresh Linux filesystem, not a +synthetic small-file fixture. All author names/emails, exact author dates and +timezones, complete messages, ordered trailers and code patch IDs were +preserved. The resulting tree was +`b1cd6a5c0b39d1e443177f7ef0d91fdbcf3a4720`, with all four snapshots matching +the manifest and only those paths plus the declared `MAINTAINERS` addition +changed from v8. Index/worktree, whitespace and connectivity checks were clean. + +The correction keeps the established 446-line hardware auto-exposure ceiling +when an HDR-capable model is in HDR subtraction mode and automatic/manual +exposure changes. Other modes retain their prior frame-derived maximum. +Manual register values, register order, frame timing, GPIO policy and the +VD55G0 illumination gate are unchanged. + +- Independent tests extracted the actual source functions, rather than a + rewritten model. The corrected source passed 779/779 cases on both host + AddressSanitizer/UndefinedBehaviorSanitizer and native ARM64 Linux. + The unchanged pre-fix source failed 176/779 HDR-sensitive cases on both. +- Cases included G0 with no HDR control, G1/G4 HDR and non-HDR operation, + control-change flag combinations, live automatic/manual transitions and + register-resolution/write failures. Stub tests do not exercise actual + hardware, firmware, V4L2 runtime locking or PM/CCI services. +- A native ARM64 `W=1` object and external test-module link passed without + warnings or errors against the previous build's read-only prepared kernel, + configuration and `Module.symvers`. No full kernel rebuild was repeated. +- The focused object SHA-256 was + `ecdf9dd3d1ab39b32abba7c64876d6bafb6c11e8ba61d01458581ac2e6576be7`; + the focused test-module SHA-256 was + `e624a4dfc7fa39486b30362b4517815860b4c09546a0d57ce8fa0eb4eaeffbc6`. + Those artifacts are not supplied as standalone installation binaries. +- The focused diff passed strict checkpatch and whitespace checks. + +## Remaining boundaries + +No Denali rear/IR graph, physical sensor identity, calibration, runtime +capture, failure recovery, system sleep or existing G1/G4 hardware regression +has been qualified. G0 nonempty `st,leds` is rejected before power resources +are used. Post-firmware GPIO input programming does not prove reset/boot pin +states or physical no-emission, electrical or optical safety. + +Inherited live VBLANK/auto-exposure-ceiling synchronization and G1/G4 LED +context behavior require separate source/sensor review. The HDR correction +does not claim to resolve those limitations or qualify illumination. diff --git a/tools/sp11-driver-audit/native-source-bundles.md b/tools/sp11-driver-audit/native-source-bundles.md index fd6df676aafc1b..eed222d2d4c208 100644 --- a/tools/sp11-driver-audit/native-source-bundles.md +++ b/tools/sp11-driver-audit/native-source-bundles.md @@ -35,6 +35,12 @@ including the separate touch-autoload repair, require their own integration and regression checks. Fork research tools and CI are not part of the native driver patch sets. +Revision 2 retains the first 23 patch emails unchanged and appends one +VD55G1-family correction: changing automatic/manual exposure must retain the +existing 446-line limit in HDR subtraction mode. It does not add illumination +support or change manual exposure, frame timing or GPIO policy. The previous +bundle remains a separately identified historical revision. + VD55G0 source is based on [Peter Marshall's native series](https://github.com/petm5/linux/commit/4a04accd5f688ac4fdca1ad4d3959ef17825085c), the existing ST driver, and @@ -72,9 +78,35 @@ make O=../sp11-native-camera-build ARCH=arm64 W=1 -j4 \ `olddefconfig` adapts to the available compiler/tools; review its configuration delta rather than calling it an identical release configuration. Cross builds -also require an appropriate `CROSS_COMPILE`. Driver-object compilation does -not link a full kernel/module, produce an installable package, or validate -CSI capture. Do not use the resulting objects with `insmod`. +also require an appropriate `CROSS_COMPILE`. These example object commands do +not produce an installable package or validate CSI capture. Do not use the +resulting objects with `insmod`. + +## Source validation, not hardware qualification + +A complete ARM64 `W=1` build of `vmlinux` and all 7,817 ordered modules passed +for the **previous 23-patch revision**, whose IR source ends at +`14bb8959fc37812a23ed55af73a2eb273acdb043`. Its configuration was unchanged +by `olddefconfig`; it was a validation configuration, not an identical release +toolchain/configuration. Warnings elsewhere in the carrier remain documented. +That full build does not include revision 2's HDR correction. + +The **current 24-patch revision** ends at IR commit +`5c961dcbcb4e7742124e402800317f693634ddb4`. Its corrected driver passed a +focused ARM64 `W=1` object and module link against that prepared kernel, plus +779 exact-function synthetic cases on each of host sanitizers and native +ARM64 Linux. The pre-correction driver failed 176 HDR-sensitive cases in the +same test set. These tests use stubs, not a sensor or the live PM/CCI core. +All 24 patch emails also applied with `git am` to the complete original v8 +commit/tree on a Linux filesystem, preserving author identities, exact dates, +commit messages and ordered trailers. The resulting four snapshots matched; +all other carrier files except the declared `MAINTAINERS` addition were +unchanged. + +See [the validation summary](native-camera-validation.md) and the bundle's +manifest for exact source, configuration and artifact hashes. Neither revision +is an installable kernel release or evidence of camera, suspend or +optical-safety qualification. ## Deliberate qualification gates @@ -94,7 +126,9 @@ IR board identity, actual I2C routing/address, supplies/reset, CSI graph, capture timing, calibration and sensor-only runtime behaviour remain to be qualified. Illuminator wiring, first-frame and live-control exposure limits, current control and independent safety requirements need a separate review -before illumination support can be added. Full capture, module linkage, -failure recovery, suspend/resume and existing VD55G1/G4 regressions remain -unproven. No device installation instructions are supplied for this -unqualified foundation. +before illumination support can be added. Live VBLANK changes still do not +explicitly synchronize the hardware auto-exposure ceiling with the changed +frame length; that inherited behavior is not corrected here. Full capture, +runtime failure recovery, suspend/resume and physical VD55G1/G4 regression +compatibility remain unproven. No device installation instructions are +supplied for this unqualified foundation. diff --git a/tools/sp11-driver-audit/ov13858-native.md b/tools/sp11-driver-audit/ov13858-native.md index 70a3725aca77ff..8b8baff80af3a5 100644 --- a/tools/sp11-driver-audit/ov13858-native.md +++ b/tools/sp11-driver-audit/ov13858-native.md @@ -94,6 +94,10 @@ the tested v8 carrier and the inspected jglathe 7.2.9 tree before these edits. the ARM64 `W=1` driver-object build passed against those carrier headers, with no Windows package present in the build environment. -These are source and driver-object checks, not a newly qualified combined -kernel. Full kernel/module linkage, integration with later carrier changes -and physical OF/ACPI regression tests remain required before a release claim. +The unchanged OV13858 snapshot was also included in the previous 23-patch +revision's complete ARM64 kernel/module link. Revision 2 adds only the +separately tested IR HDR correction; see +[the validation summary](native-camera-validation.md) for that distinction. +These checks do not qualify a combined hardware release. Integration with later +carrier changes and physical OF/ACPI regression tests remain required before a +release claim. From 04aeb0b2ff09f222bdddfa729d035c5a8985553b Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 15:54:28 +0100 Subject: [PATCH 08/17] docs: record current native camera source validation Describe the format, shared-control-lock and G0 exposure-write corrections in the third source-only camera bundle. Keep the original full-kernel build separate from subsequent exact-source tests and focused module links. Clarify that G0's manual maximum is not a proven automatic-exposure ceiling, and retain the board, firmware and hardware qualification gates. Earlier source archives remain independently identified historical records. Signed-off-by: Leon Silcott --- .../native-camera-validation.md | 74 +++++++++++++++++-- .../native-source-bundles.md | 55 ++++++++------ tools/sp11-driver-audit/ov13858-native.md | 4 +- 3 files changed, 101 insertions(+), 32 deletions(-) diff --git a/tools/sp11-driver-audit/native-camera-validation.md b/tools/sp11-driver-audit/native-camera-validation.md index 01982cfdc49e50..fbe67a67df045a 100644 --- a/tools/sp11-driver-audit/native-camera-validation.md +++ b/tools/sp11-driver-audit/native-camera-validation.md @@ -26,13 +26,14 @@ contained 2 OV13858 and 21 IR patches. Its IR source ended at - Its `vmlinux` SHA-256 was `83a4abe6b3a00e17c270c6820d88c99e441b1fe196816246cfc656106bf8d31c`. -That complete build did **not** contain the HDR correction added in revision 2. +That complete build did **not** contain the HDR correction added in revision 2 +or the three corrections added in revision 3. -## Current revision: HDR correction +## Revision 2: HDR correction Revision 2 retains all 23 earlier patch emails byte-for-byte and appends IR commit `5c961dcbcb4e7742124e402800317f693634ddb4`, for 24 patches total. -The current IR driver SHA-256 is +That revision's IR driver SHA-256 is `086ec2feff305a159d93a283c0e303fd04108c798e6c4252d34bfe5e45c9b5b1`. The other three driver/binding snapshots are unchanged. @@ -45,7 +46,7 @@ preserved. The resulting tree was the manifest and only those paths plus the declared `MAINTAINERS` addition changed from v8. Index/worktree, whitespace and connectivity checks were clean. -The correction keeps the established 446-line hardware auto-exposure ceiling +The correction keeps the existing 446-line maximum-register setting when an HDR-capable model is in HDR subtraction mode and automatic/manual exposure changes. Other modes retain their prior frame-derived maximum. Manual register values, register order, frame timing, GPIO policy and the @@ -69,6 +70,55 @@ VD55G0 illumination gate are unchanged. Those artifacts are not supplied as standalone installation binaries. - The focused diff passed strict checkpatch and whitespace checks. +## Current revision: format, control lifecycle and G0 exposure writes + +Revision 3 retains all 24 revision-2 patch emails byte-for-byte and appends: + +- `62ff85ae8cc10d44040b293bea3cf887eff65d09`: reject active format changes + while the core still owns a stream, including after a failed stop. TRY + negotiation remains available. The exact source passed 209 synthetic cases + per host-sanitizer/native-ARM64 platform; these model the surrounding core. +- `ef08c6a681e2cf61feb5aa5189bf51368401b6f0`: initialize the handler before + choosing the shared state lock, copy initial state before control + construction, check sticky constructor errors and release state before its + handler. Stream callbacks use caller-locked grab helpers. The exact source + passed 298 synthetic lifecycle cases per platform. Separate real-mutex + cases distinguished the corrected helpers from a deadlocking intermediate; + they do not constitute full-kernel lockdep or real V4L2 allocator tests. +- `ff99e470cc8ca665580cd26bd072145d853d1ab5`: omit the newly added G0 + maximum-register write and remove its mapping, restoring the native ST G0 + control-write pattern. Mode selection, manual exposure/gains, software + ranges and G1/G4/VD65G4 register traces remain unchanged. + +The current IR driver SHA-256 is +`600064057065a8a7a98951fa016b22dda842c634bf3832a985f2583eb488e1a0`. +The other three source snapshots and six licence/copying files are unchanged. +All 27 emails replayed onto the complete original v8 tree on a Linux filesystem, +with author identities, exact dates, messages, ordered trailers and stable +code patch IDs preserved. Only the four snapshots and declared `MAINTAINERS` +addition differ from the carrier. The manifest records the resulting tree. + +The final G0 correction passed 1,059/1,059 exact-function cases on both host +AddressSanitizer/UndefinedBehaviorSanitizer and native ARM64 GCC 13.3. The +unchanged parent failed 76 intended G0-only assertions, not 76 demonstrated +physical defects. All 896 success-matrix traces differ only by deleting the +G0 `0x096c` operation; all 912 non-G0 cases, including injected failures, are +unchanged. The harness uses actual CCI encodings, maps, resolver and write/ +control functions, but a stub bus and projected control/model structures. +It proves traces and first-error handling, not register acceptance or AE +efficacy. Missing G0 mapping returns `-EOPNOTSUPP` without a CCI transaction. + +A focused native ARM64 `W=1` object/module link for this exact final source +passed without warnings or errors against the read-only prepared kernel, +configuration and `Module.symvers`. The object SHA-256 is +`17859f8fb91b595577ce28fc002fcbbb31f3f51227ccf4555e828be33a5600ab`; +the test-module SHA-256 is +`ad15bc5252eadba578b53c8e2f2c18f0c94b72a9ae2a0be54c9e18b311d8d8cf`. +No complete kernel build was repeated for revision 3. These test artifacts +are not standalone installation modules. The final diff passed strict +checkpatch and whitespace checks; OpenCode GLM 5.1 found no source-level +blocker in the bounded correction. + ## Remaining boundaries No Denali rear/IR graph, physical sensor identity, calibration, runtime @@ -77,6 +127,16 @@ has been qualified. G0 nonempty `st,leds` is rejected before power resources are used. Post-firmware GPIO input programming does not prove reset/boot pin states or physical no-emission, electrical or optical safety. -Inherited live VBLANK/auto-exposure-ceiling synchronization and G1/G4 LED -context behavior require separate source/sensor review. The HDR correction -does not claim to resolve those limitations or qualify illumination. +ST's pinned native G0 source, with the same cut1 firmware 2.11 and cut2 0.5 +payloads, never directly writes `0x096c`. Public documentation describes that +register as a manual maximum in a standby-configured group; this is not proof +of an AUTO ceiling or a firmware-specific live-write exception. The current +source restores the vendor-native absence of that host operation, not full +vendor-driver equivalence or physical exposure-range validation. + +Live VBLANK changes continue to update context frame length and software +exposure ranges. Register latching, full manual range, G1-family live +static-register writes, partial-write recovery and LED context behavior +require separate source/sensor review. No speculative startup-only maximum +write or group-hold transaction is added. None of these changes qualifies +illumination. diff --git a/tools/sp11-driver-audit/native-source-bundles.md b/tools/sp11-driver-audit/native-source-bundles.md index eed222d2d4c208..699903d6afa1da 100644 --- a/tools/sp11-driver-audit/native-source-bundles.md +++ b/tools/sp11-driver-audit/native-source-bundles.md @@ -35,11 +35,12 @@ including the separate touch-autoload repair, require their own integration and regression checks. Fork research tools and CI are not part of the native driver patch sets. -Revision 2 retains the first 23 patch emails unchanged and appends one -VD55G1-family correction: changing automatic/manual exposure must retain the -existing 446-line limit in HDR subtraction mode. It does not add illumination -support or change manual exposure, frame timing or GPIO policy. The previous -bundle remains a separately identified historical revision. +Revision 3 retains revision 2's 24 patch emails unchanged and appends three +IR-driver corrections: reject active format changes while streaming, establish +the shared state/control lock before publication and use its caller-locked +control helpers, and restore VD55G0's native exposure-control write sequence. +The bundle contains 2 OV13858 and 25 IR patches. Earlier archives remain +separately identified, immutable historical revisions. VD55G0 source is based on [Peter Marshall's native series](https://github.com/petm5/linux/commit/4a04accd5f688ac4fdca1ad4d3959ef17825085c), @@ -89,22 +90,26 @@ for the **previous 23-patch revision**, whose IR source ends at `14bb8959fc37812a23ed55af73a2eb273acdb043`. Its configuration was unchanged by `olddefconfig`; it was a validation configuration, not an identical release toolchain/configuration. Warnings elsewhere in the carrier remain documented. -That full build does not include revision 2's HDR correction. - -The **current 24-patch revision** ends at IR commit -`5c961dcbcb4e7742124e402800317f693634ddb4`. Its corrected driver passed a -focused ARM64 `W=1` object and module link against that prepared kernel, plus -779 exact-function synthetic cases on each of host sanitizers and native -ARM64 Linux. The pre-correction driver failed 176 HDR-sensitive cases in the -same test set. These tests use stubs, not a sensor or the live PM/CCI core. -All 24 patch emails also applied with `git am` to the complete original v8 +That full build does not include revision 2's HDR correction or revision 3's +three corrections. + +The **current 27-patch revision** ends at IR commit +`ff99e470cc8ca665580cd26bd072145d853d1ab5`. Its exact driver passed a focused +ARM64 `W=1` object/module link against the prepared kernel and 1,059 synthetic +register-trace/error cases on each of host sanitizers and native ARM64 Linux. +The G0 correction removes only the added maximum-register operation; all +tested G1/G4/VD65G4 traces, including HDR subtraction's 446-line value, remain +unchanged. Separate checks at the preceding format and lifecycle commits +cover active-state mutation, allocation failures and shared-lock ownership. +These are source-level tests, not sensor or full V4L2/PM/CCI-core tests. + +All 27 patch emails applied with `git am` to the complete original v8 commit/tree on a Linux filesystem, preserving author identities, exact dates, commit messages and ordered trailers. The resulting four snapshots matched; -all other carrier files except the declared `MAINTAINERS` addition were -unchanged. +only those paths and the declared `MAINTAINERS` addition differ from v8. See [the validation summary](native-camera-validation.md) and the bundle's -manifest for exact source, configuration and artifact hashes. Neither revision +manifest for exact source, configuration and artifact hashes. No revision is an installable kernel release or evidence of camera, suspend or optical-safety qualification. @@ -126,9 +131,13 @@ IR board identity, actual I2C routing/address, supplies/reset, CSI graph, capture timing, calibration and sensor-only runtime behaviour remain to be qualified. Illuminator wiring, first-frame and live-control exposure limits, current control and independent safety requirements need a separate review -before illumination support can be added. Live VBLANK changes still do not -explicitly synchronize the hardware auto-exposure ceiling with the changed -frame length; that inherited behavior is not corrected here. Full capture, -runtime failure recovery, suspend/resume and physical VD55G1/G4 regression -compatibility remain unproven. No device installation instructions are -supplied for this unqualified foundation. +before illumination support can be added. G0's `0x096c` is described as a +manual maximum, not a proven automatic-exposure ceiling. The current source +does not directly access it, matching ST's native driver for the same firmware +versions. This does not establish the full physical exposure range. Live +VBLANK changes update context frame length and software exposure ranges; +firmware-specific maximum/latching rules and the G1-family live static-register +policy still need qualification. No guessed group-hold or startup-only maximum +write has been added. Full capture, runtime failure recovery, suspend/resume +and physical VD55G1/G4 regression compatibility remain unproven. No device +installation instructions are supplied for this unqualified foundation. diff --git a/tools/sp11-driver-audit/ov13858-native.md b/tools/sp11-driver-audit/ov13858-native.md index 8b8baff80af3a5..1cfb5161ebcdd2 100644 --- a/tools/sp11-driver-audit/ov13858-native.md +++ b/tools/sp11-driver-audit/ov13858-native.md @@ -95,8 +95,8 @@ the tested v8 carrier and the inspected jglathe 7.2.9 tree before these edits. with no Windows package present in the build environment. The unchanged OV13858 snapshot was also included in the previous 23-patch -revision's complete ARM64 kernel/module link. Revision 2 adds only the -separately tested IR HDR correction; see +revision's complete ARM64 kernel/module link. Revisions 2 and 3 add separately +tested IR-driver corrections without changing this sensor snapshot; see [the validation summary](native-camera-validation.md) for that distinction. These checks do not qualify a combined hardware release. Integration with later carrier changes and physical OF/ACPI regression tests remain required before a From e4efe2251695752042cfe16c4933a453bdd42e59 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 16:45:40 +0100 Subject: [PATCH 09/17] docs: record native Bluetooth provisioning source checks Describe the separately licensed bounded dtbloader source candidate, its complete native ARM64 COFF link and synthetic reader/adapter coverage. Keep factory-source, partition selection, FDT publication and installed-provider qualification explicitly open. This does not add a kernel partition reader, enable a device or authorize EFI installation. Source notes contain no private captures, local tool paths or provisioning addresses. Signed-off-by: Leon Silcott --- tools/sp11-driver-audit/README.md | 6 ++ .../native-bluetooth-provisioning.md | 72 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tools/sp11-driver-audit/native-bluetooth-provisioning.md diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index 26ad62278e0848..269cd610240fe4 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -124,6 +124,7 @@ registers. Neither check establishes CSI timing or successful rear capture. | Sensors | Surface SAM support is distinct from the Qualcomm sensor hub | Transport, exact board configuration, orientation and calibration | | USB4 | Router/retimer groundwork exists | Resource ownership, sideband and working firmware/ring lifecycle | | Audio | Working protected kernel and userspace path; an ALSA update replaced the selector | Preserve sound while completing persistent userspace selection and scoped compatibility | +| Bluetooth factory identity | Existing native firmware-property consumer; separate bounded provider source candidate | Authenticate factory source, harden provider selection and qualify the installed boot path | Hardware identity comes from the actual board, firmware tables and device identification. The bundle includes optional docks, modems, SSDs, panels and @@ -140,6 +141,11 @@ provenance and the source, object and module validation workflow. They are foundations for qualification, not installable camera fixes. In particular, a native IR sensor driver is not equivalent to safely enabling an illuminator. +The [native Bluetooth provisioning source track](native-bluetooth-provisioning.md) +records a separate source-only dtbloader candidate, its exact native build and +synthetic coverage, and the provider/boot-chain gates that remain. It does not +add a kernel raw-partition reader or change the running device's factory identity. + The research checkout began at the older integration revision `2cbd1ec3e2da385e7bd91fd65c63ba5a8fb5b865`. These tools are independent of that kernel revision and are fork research infrastructure. Hardware changes must diff --git a/tools/sp11-driver-audit/native-bluetooth-provisioning.md b/tools/sp11-driver-audit/native-bluetooth-provisioning.md new file mode 100644 index 00000000000000..72def5c03a5d42 --- /dev/null +++ b/tools/sp11-driver-audit/native-bluetooth-provisioning.md @@ -0,0 +1,72 @@ +# Native Bluetooth factory-provisioning source track + +This is a separate native firmware-provider research track, not a new kernel +partition driver, a Windows compatibility layer or an installation procedure. +The current candidate has not been executed in EFI or on real factory storage. + +## Existing Linux boundary + +The kernel already has a firmware-node `local-bd-address` consumer in +`net/bluetooth/hci_sync.c`. Qualcomm's driver also has an explicit +`qcom,local-bd-address-broken` byte-order quirk. Those native interfaces provide +an integration boundary; their presence does not prove the current board's +property, byte order or installed provider is correct. Do not add or remove +that quirk by guesswork, invent a replacement factory address or start scanning +raw storage from a new Linux driver to work around unverified boot integration. + +## Native source candidate + +The source-only `sp11-native-dpp-source-20261015` bundle applies two focused +commits to [dtbloader](https://github.com/TravMurav/dtbloader) at +`b1bf23e88b779ac323fbcbe547cf3ba939433df3`: + +1. `587ded14d0f8ada9324bd325bac9faef31696c8f`: isolate the known native RWFS + fields in a bounded, allocation-free reader; check media extents/identity, + exact payload lengths and read budgets; remove raw provisioning debug output. +2. `397ee3ca597084d91f1be5c82fe10129e03f43a4`: constrain Bluetooth vendor matching + to `QCOM` through the existing Surface Pro 11 descriptor, preserving other + boards' unconstrained vendor policy and the existing WLAN/byte-reversal path. + +The reader interprets only the licensed native implementation's known fields. +Unknown versions, secondary magic, region ownership and nonzero row states +remain opaque. The first zero-present row ends traversal; duplicates are +rejected within that visited prefix. Row/byte limits are implementation policy, +not assertions about factory-format maxima. No proprietary format header, +Windows binary, recovered pseudocode or real provisioning record is included. +The original BSD-3-Clause licence and contributors' attribution are retained. + +## What the checks establish + +The actual standalone core passed 4,691 synthetic calls and 300,856 assertions +per optimized and sanitizer host run. The actual EFI adapter/Surface wrapper, +included with fake EFI/FDT boundaries, passed 87 scenarios and 2,295 assertions +per run. These include partial reads, failure-zero output, changed media, +vendor scoping, byte reversal and the exact row-cap/sentinel boundary. + +The complete native project compiled and linked with the exact recorded +GNU-EFI/libfdt/SHA-1 submodules and Clang/LLD 18.1.3. All 85 expected objects +were independently checked as ARM64 COFF; the PE32+ image has the EFI +boot-service-driver subsystem and no import/delay-import tables. Project +sources used `-Wall -Werror`, with no warnings in the complete build log. +This build is freestanding native EFI: `aarch64-windows` selects COFF/ABI, not +a Windows runtime. The unsigned research image is not distributed in the bundle. + +OpenCode's completed GLM 5.1 review found no new actionable defects in the +supplied production source and recommended source-only status. Its requested +tests were mapped to actual coverage; review imprecision was qualified rather +than used as authority. No source review or compilation proves device parity. + +## Remaining deployment gates + +The existing GPT-name comparison and broad/ambiguous partition selection still +need hardening. A stable media ID does not authenticate storage or rule out +in-place mutation. WLAN/Bluetooth success remains coupled, and the two FDT +updates remain sequential: a failed second update leaves the first applied. +Valid existing addresses, exact compatible-node selection, actual factory +record semantics and the installed Stubble/provider path need independent +qualification. The host adapter tests intentionally reproduce the partial-FDT +failure instead of claiming transactional publication. + +No raw-storage capture, provider invocation, boot-order/Secure Boot change or +hardware radio test follows from these source checks. Those require separately +scoped decisions after the installed boot path and record ownership are known. From 943311036aa72f4151006b74e20abb540ff9a386 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 17:14:14 +0100 Subject: [PATCH 10/17] docs: record native source build verification and ABI gates Record the completed clean build of the exact 27-patch camera source, independent module mapping and artifact checks, and explicit review of quotation-only warning differences. Preserve the failed checker result and immutable source archive as historical evidence. Describe the separate native board-priority correction, synthetic regression scope and versioned EFI protocol compatibility gate. Keep sized-input validation, staged FDT publication, factory ownership and hardware qualification explicitly outstanding. Signed-off-by: Leon Silcott --- .../native-bluetooth-provisioning.md | 56 +++++++++++++++++++ .../native-camera-validation.md | 47 ++++++++++++++-- .../native-source-bundles.md | 15 ++++- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/tools/sp11-driver-audit/native-bluetooth-provisioning.md b/tools/sp11-driver-audit/native-bluetooth-provisioning.md index 72def5c03a5d42..9fb3b1c59cc836 100644 --- a/tools/sp11-driver-audit/native-bluetooth-provisioning.md +++ b/tools/sp11-driver-audit/native-bluetooth-provisioning.md @@ -56,6 +56,46 @@ supplied production source and recommended source-only status. Its requested tests were mapped to actual coverage; review imprecision was qualified rather than used as authority. No source review or compilation proves device parity. +## Follow-up: declared board-selection priority + +The separate native commit `0d88509c82a3752fa3b9afb982b344612f1dcc31` +corrects a pre-existing board matcher issue: use `hwids[priority[i]]`, not +`hwids[i]`. The original loop checked IDs 0 through 8, ignoring the declared +specificity order and omitting IDs 9 through 11. The priority array, device +descriptors, additional match predicates, registration-order ties and +successful-match cache are unchanged. This commit is not part of the immutable +two-patch source bundle described above. + +The exact function passed 191 synthetic scenarios per optimized and +AddressSanitizer/UndefinedBehaviorSanitizer run on both macOS and native ARM64 +Linux. Reversing only the indexed expression reproduced the same 75 expected +failures per run. Cases cover every declared priority pair in both registry +orders, sole identities, rejected predicates, ties, missing identities and +cache/error retry behavior. Actual project headers are used, but EFI services, +SMBIOS population and registry boundaries are synthetic; these tests do not +exercise the COFF linker registry or identify a real board. + +The corrected source also completed the full 85-object native ARM64 EFI +compile/link without warnings. OpenCode recommended the focused correction +and these source-level gates, not deployment of the unqualified provider. + +## Versioned protocol gate + +The inspected dtbloader header and +[Stubble's pinned header](https://github.com/ubuntu/stubble/blob/08218605b5ac014f7064ef7129e84ba7ec582dca/include/proto/dt-fixup.h) +agree on the legacy `e617d64c-fe08-46da-f4dc-bbd5870c7300` interface with four +arguments, including flags. Stubble's official main still had that exact +revision when checked on 15 October 2026. This source agreement does not identify +the provider or consumer installed on a particular device. + +The [published protocol specification](https://github.com/U-Boot-EFI/EFI_DT_FIXUP_PROTOCOL/blob/c98cfac7b4daed69bb097a579981b877b765569a/README.rst) +records an incompatible January 2026 change: GUID +`60ed6ba9-dfef-4799-ac7b-75e0f833456c`, three arguments and no flags/reservation +operation. The protocol revision value alone does not distinguish these ABIs. +Do not change only the GUID, cast between signatures or add speculative memory +reservation behavior. Pin the actual installed caller/provider pair before any +separate migration decision. Current source checks preserve the legacy ABI. + ## Remaining deployment gates The existing GPT-name comparison and broad/ambiguous partition selection still @@ -67,6 +107,22 @@ record semantics and the installed Stubble/provider path need independent qualification. The host adapter tests intentionally reproduce the partial-FDT failure instead of claiming transactional publication. +The sole current GPT-name caller passes the fixed literal `DPP`; the pinned +native comparison reads at most four units of its 36-unit media name. A current +GPT-name overread has not been demonstrated. Generic API hardening must not be +reported as an observed exploit. Separately, the native board matcher declares +a hardware-ID priority array; the follow-up above corrects its indexed lookup. +Actual SMBIOS generation and device selection still require qualification +before provider deployment. + +The next publication change must establish complete, successful file reads and +validate each FDT against its actual supplied byte extent before staging any +mutation. A header's `totalsize` does not establish accessible buffer capacity. +Use the actual bounded libfdt validator and retain the existing caller-owned +buffer; a failed update must not expose earlier WLAN or Bluetooth mutations. +This source architecture remains work to implement and test, not a guarantee +of the current candidate. + No raw-storage capture, provider invocation, boot-order/Secure Boot change or hardware radio test follows from these source checks. Those require separately scoped decisions after the installed boot path and record ownership are known. diff --git a/tools/sp11-driver-audit/native-camera-validation.md b/tools/sp11-driver-audit/native-camera-validation.md index fbe67a67df045a..5aaabce238f307 100644 --- a/tools/sp11-driver-audit/native-camera-validation.md +++ b/tools/sp11-driver-audit/native-camera-validation.md @@ -17,8 +17,9 @@ contained 2 OV13858 and 21 IR patches. Its IR source ended at `make ARCH=arm64 W=1 -j4 vmlinux modules`. - `vmlinux` and all 7,817 modules listed in `modules.order` were present and identified as little-endian AArch64 ELF files. The modified drivers had no - compiler warnings or errors. There were 1,308 compiler-warning lines - elsewhere in the carrier; this was not a warning-free whole-kernel build. + compiler warnings or errors. Warnings elsewhere in the carrier are + classified in the subsequent-build comparison below; this was not a + warning-free whole-kernel build. - The effective configuration was unchanged by `olddefconfig`, SHA-256 `bccba597adeb6475571f813c278ca02e6a478c2134f862cf64e1319ada10bdf7`. It enabled both sensor drivers as modules for validation and disabled debug @@ -114,11 +115,49 @@ configuration and `Module.symvers`. The object SHA-256 is `17859f8fb91b595577ce28fc002fcbbb31f3f51227ccf4555e828be33a5600ab`; the test-module SHA-256 is `ad15bc5252eadba578b53c8e2f2c18f0c94b72a9ae2a0be54c9e18b311d8d8cf`. -No complete kernel build was repeated for revision 3. These test artifacts -are not standalone installation modules. The final diff passed strict +At bundle creation, revision 3 had only the focused build above; the later +complete build is recorded below. These test artifacts are not standalone +installation modules. The final diff passed strict checkpatch and whitespace checks; OpenCode GLM 5.1 found no source-level blocker in the bounded correction. +## Revision 3: subsequent complete kernel and module link + +The exact 27-patch replay, tree +`aa48347a793826819475a078eee05759fadc7f26`, subsequently completed a clean +native ARM64 GCC 13.3.0/binutils 2.42 build with +`make ARCH=arm64 W=1 -j4 vmlinux modules`. The same validation configuration +remained unchanged by `olddefconfig`. This includes all four source corrections +added after the earlier complete build, not just the original 23 patches. + +Independent read-only checks verified `vmlinux` and all 7,817 ordered modules +as ELF64 little-endian AArch64 files, with unique canonical paths, no object/ +module aliases and recorded hashes. The frozen build wrapper had returned a +post-build verification error because it expected `.ko` entries in +`modules.order`. The kernel's own `scripts/Makefile.modfinal` instead maps its +listed `.o` entries to `.ko`. A separate corrected checker verified every +mapped output; neither the build nor its original failed report was replaced. + +Both full logs contain 1,307 source-positioned compiler warnings and 2,467 raw +warning-colon lines, including diagnostics without source positions. Neither +modified sensor appears in those warning lines. The ten category counts match. +Review of all added/removed signatures found only typographic versus ASCII +apostrophes; complete warning multisets match after that exact normalization. +This is explicit review of a presentation difference, not a warning-free build +or an assertion that existing carrier warnings are harmless. + +The complete-build artifact SHA-256 values are: + +- `vmlinux`: `f73d23291d20eca9c5305b61a06923274089364aacd37b5639c53028a8a748ed`. +- `ov13858.ko`: `bb4f5551712351a307e9b5d87bae02d9f22b4e77297a9945676ecd4add35fb69`. +- `vd55g1.ko`: `902d482c62721a8552f8f042adf35602a5417452b441da881e766f72bfa47abd`. + +The existing source archive remains immutable. These later checks qualify +compilation/linking of its exact source, not Debian packaging, binding or +`MAINTAINERS` semantics, release-toolchain equivalence, installation, capture, +firmware behavior or physical hardware safety. Generated signing material was +not read, hashed or exported by the audit. + ## Remaining boundaries No Denali rear/IR graph, physical sensor identity, calibration, runtime diff --git a/tools/sp11-driver-audit/native-source-bundles.md b/tools/sp11-driver-audit/native-source-bundles.md index 699903d6afa1da..e3177b817bb004 100644 --- a/tools/sp11-driver-audit/native-source-bundles.md +++ b/tools/sp11-driver-audit/native-source-bundles.md @@ -94,9 +94,18 @@ That full build does not include revision 2's HDR correction or revision 3's three corrections. The **current 27-patch revision** ends at IR commit -`ff99e470cc8ca665580cd26bd072145d853d1ab5`. Its exact driver passed a focused -ARM64 `W=1` object/module link against the prepared kernel and 1,059 synthetic -register-trace/error cases on each of host sanitizers and native ARM64 Linux. +`ff99e470cc8ca665580cd26bd072145d853d1ab5`. Its exact source subsequently +completed a clean full ARM64 `W=1` build of `vmlinux` and all 7,817 ordered +modules, independently checked against the unchanged validation configuration +and source pins. No modified-driver warnings were found. Existing carrier +warning counts/categories match the earlier log; differing signatures were +individually reconciled as quotation-mark presentation only. A post-build +checker correction follows the kernel's actual `.o`-to-`.ko` ordering contract; +the original failed checker report and immutable archive remain preserved. + +The exact driver also passed a focused ARM64 `W=1` object/module link against +the prepared kernel and 1,059 synthetic register-trace/error cases on each of +host sanitizers and native ARM64 Linux. The G0 correction removes only the added maximum-register operation; all tested G1/G4/VD65G4 traces, including HDR subtraction's 446-line value, remain unchanged. Separate checks at the preceding format and lifecycle commits From 5187f9470b93abdcc31286ece8350e60ee65c74c Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 17:36:37 +0100 Subject: [PATCH 11/17] docs: record native source replay and scoped camera schema checks Identify the complete four-patch native provisioning source bundle and its immutable archive digest, exact-base replay and public test coverage. Retain the previous archive and distinguish source-only evidence from installed-provider or hardware support. Record direct checks for the two camera binding snapshots, generated examples and nine positive/negative schema controls. Inspect JSON diagnostics rather than treating make or validator exit status as sufficient. Keep unrelated carrier diagnostics and physical qualification gates explicit. Signed-off-by: Leon Silcott --- .../native-bluetooth-provisioning.md | 29 +++++++++++++++++-- .../native-camera-validation.md | 26 +++++++++++++++++ .../native-source-bundles.md | 6 +++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/tools/sp11-driver-audit/native-bluetooth-provisioning.md b/tools/sp11-driver-audit/native-bluetooth-provisioning.md index 9fb3b1c59cc836..92155038f5bdc0 100644 --- a/tools/sp11-driver-audit/native-bluetooth-provisioning.md +++ b/tools/sp11-driver-audit/native-bluetooth-provisioning.md @@ -16,7 +16,7 @@ raw storage from a new Linux driver to work around unverified boot integration. ## Native source candidate -The source-only `sp11-native-dpp-source-20261015` bundle applies two focused +The source-only `sp11-native-dpp-source-20261015-r2` bundle applies four focused commits to [dtbloader](https://github.com/TravMurav/dtbloader) at `b1bf23e88b779ac323fbcbe547cf3ba939433df3`: @@ -26,6 +26,25 @@ commits to [dtbloader](https://github.com/TravMurav/dtbloader) at 2. `397ee3ca597084d91f1be5c82fe10129e03f43a4`: constrain Bluetooth vendor matching to `QCOM` through the existing Surface Pro 11 descriptor, preserving other boards' unconstrained vendor policy and the existing WLAN/byte-reversal path. +3. `0d88509c82a3752fa3b9afb982b344612f1dcc31`: honor the native matcher's + already declared hardware-ID priority order without changing descriptors. +4. `3ba7a4bdfb92bf9869ab3783ccbe06a036dc86ce`: add portable actual-function + board-matcher tests alongside the existing bounded-reader tests. + +Revision 2 is the complete four-patch series from the same base, not an +incremental patch for the original two-patch archive. That earlier archive +remains an immutable historical revision. Replaying the four emails onto the +full exact base on Linux reproduced tree +`a91c60a401d726276d56e51d3e9a971e3c083c48`, with all 15 review snapshots and +original author identities, exact dates, messages, trailers and code patch IDs +preserved. Replay committer metadata need not match the original commits. + +The revision-2 archive SHA-256 is +`c6910c329bb9edc553c8ec6eb62d01a847e92eac272596009ddd35a6dada1a57`. +It contains only 23 text files, including 15 source/licence snapshots and four +patch emails. Deterministic packing, exact member-byte comparison and fresh +Linux extraction verified all 22 checksums. Private evidence and compiled +results are not distributed in the archive. The reader interprets only the licensed native implementation's known fields. Unknown versions, secondary magic, region ownership and nonzero row states @@ -63,8 +82,8 @@ corrects a pre-existing board matcher issue: use `hwids[priority[i]]`, not `hwids[i]`. The original loop checked IDs 0 through 8, ignoring the declared specificity order and omitting IDs 9 through 11. The priority array, device descriptors, additional match predicates, registration-order ties and -successful-match cache are unchanged. This commit is not part of the immutable -two-patch source bundle described above. +successful-match cache are unchanged. The correction and its portable tests +are included in revision 2, not retroactively added to the original archive. The exact function passed 191 synthetic scenarios per optimized and AddressSanitizer/UndefinedBehaviorSanitizer run on both macOS and native ARM64 @@ -75,6 +94,10 @@ cache/error retry behavior. Actual project headers are used, but EFI services, SMBIOS population and registry boundaries are synthetic; these tests do not exercise the COFF linker registry or identify a real board. +The public `make -C tests check` and `sanitize` targets reproduce both suites; +select the installed C compiler with `CC`, such as `CC=clang-18`. Python 3 is +needed for test extraction/orchestration only, not the production provider. + The corrected source also completed the full 85-object native ARM64 EFI compile/link without warnings. OpenCode recommended the focused correction and these source-level gates, not deployment of the unqualified provider. diff --git a/tools/sp11-driver-audit/native-camera-validation.md b/tools/sp11-driver-audit/native-camera-validation.md index 5aaabce238f307..87fdd0155b13f2 100644 --- a/tools/sp11-driver-audit/native-camera-validation.md +++ b/tools/sp11-driver-audit/native-camera-validation.md @@ -158,6 +158,32 @@ compilation/linking of its exact source, not Debian packaging, binding or firmware behavior or physical hardware safety. Generated signing material was not read, hashed or exported by the audit. +## Scoped binding and example checks + +The same exact two binding snapshots subsequently passed direct document +validation, YAML lint and the kernel's binding-style checker using dtschema +2026.9. Each direct check exited successfully without diagnostics. The selected +kernel `dt_binding_check` target generated both example DTBs and all three check +stamps without changing source. + +Separate checks confirmed both schema IDs were present in the processed schema +and inspected JSON validation diagnostics, not just command exit status. In +this tool version, `dt-validate` returns zero even for validation errors, and +the kernel example recipe masks that exit with `true`. + +Four synthetic examples passed: the original OV13858 and G1 examples, plus G0 +without LEDs using 600 MHz and the 375 MHz schema-minimum link frequencies. +Five controls were +rejected by the intended schemas: missing `reg` on either original sensor, +OV13858 at the unsupported 592.8 MHz link frequency, G0 with `st,leds`, and G0 +below 375 MHz. These checks cover the stated example constraints, not every possible +tree, driver behavior or physical clock/sensor configuration. + +Whole-tree schema aggregation also reported an unrelated ath12k parsing error, +duplicate EC-compatible schemas and a jobserver warning. These remain recorded; +the check is not described as a clean validation of every carrier binding. +No board graph, camera operation, electrical or optical behavior was qualified. + ## Remaining boundaries No Denali rear/IR graph, physical sensor identity, calibration, runtime diff --git a/tools/sp11-driver-audit/native-source-bundles.md b/tools/sp11-driver-audit/native-source-bundles.md index e3177b817bb004..7d3e8b40958e9c 100644 --- a/tools/sp11-driver-audit/native-source-bundles.md +++ b/tools/sp11-driver-audit/native-source-bundles.md @@ -118,7 +118,11 @@ commit messages and ordered trailers. The resulting four snapshots matched; only those paths and the declared `MAINTAINERS` addition differ from v8. See [the validation summary](native-camera-validation.md) and the bundle's -manifest for exact source, configuration and artifact hashes. No revision +manifest for exact source, configuration and artifact hashes. The exact binding +snapshots also passed scoped document/lint/style checks and four positive/five +negative synthetic example checks; unrelated carrier schema diagnostics remain +recorded. These are not physical board or exhaustive schema-semantic tests. +No revision is an installable kernel release or evidence of camera, suspend or optical-safety qualification. From 7b7fddfe48fdc4ead2c21d2bf1b0480e55c17c4d Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 18:05:19 +0100 Subject: [PATCH 12/17] Record sized native DTB publication source checks Document the separate staged-publication source commit, portable host test targets, full native link and independent evidence audit. Keep the immutable revision-2 archive distinct and retain explicit opening-error, factory-data, protocol and hardware qualification gates. Signed-off-by: Leon Silcott --- .../native-bluetooth-provisioning.md | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/tools/sp11-driver-audit/native-bluetooth-provisioning.md b/tools/sp11-driver-audit/native-bluetooth-provisioning.md index 92155038f5bdc0..2f57a0daeb90aa 100644 --- a/tools/sp11-driver-audit/native-bluetooth-provisioning.md +++ b/tools/sp11-driver-audit/native-bluetooth-provisioning.md @@ -121,14 +121,59 @@ separate migration decision. Current source checks preserve the legacy ABI. ## Remaining deployment gates +### Separate follow-up: sized inputs and staged publication + +Local native commit `3489cd54a96f1a1e9397894bef101da220eb3e10` adds the sized +input/staging prerequisite on a separate branch over the four-commit source +above. Its tree is `86123f3325150775539e3ba3ae16f6a69b873b76`. It is not yet +included in the immutable revision-2 source archive or a firmware release. + +The file path now checks a bounded metadata query and exact read, validates +against the bytes actually read and closes file/volume handles. A shared +helper stages the complete existing callback on a private bounded tree, then +validates, packs and validates again before copying the packed extent back. +Failures leave the original buffer unchanged under the documented accessible, +exclusively owned memory/callback contract. Successful calls preserve the +pointer and unused tail. This is not concurrency-atomic or a whole-firmware/ +NVRAM transaction, and does not change factory-data or board-selection policy. + +The separate native ADR records the 2 MiB AArch64 boot/workspace ceiling, +existing 1 MiB local allocation, legacy protocol ABI/headroom negotiation and +successfully published ACPI-page ownership. Zeroing the local allocation before +reading makes pre-fixup normalized-hash padding deterministic; it does not +introduce authentication or change confirmation/NVRAM policy. Metadata uses a +bounded pool, avoiding a large stack frame and an unavailable stack-probe +helper without adding a Windows runtime dependency. + +All six public targets (`check`, `sanitize`, `check-fdt`, `sanitize-fdt`, +`check-io`, `sanitize-io`) passed on ARM64 macOS and native ARM64 Linux. The +optional FDT suite exercises 95 scenarios/795 assertions with real libfdt; +the file-helper suite exercises 2,802 calls with real GNU-EFI types and fake +allocation/file callbacks. Neither target fetches dependencies. Six and eight +malformed source-extraction controls respectively fail closed. The original +standalone core/matcher suites still pass with unchanged test dependencies. + +Separate private actual-source fixtures additionally cover loader lifecycle +with 52 scenarios and the historical partial-mutation negative control. These +do not qualify actual filesystem opening, SHA/NVRAM authorization or factory +provisioning. The full native 86-object ARM64 COFF link passed without warnings; +retained source/object/image bytes were independently audited. OpenCode's +direct GLM 5.1 supplied-source review found no introduced blocker. Its review +and these tests are not proof of firmware ABI, deployment or hardware parity. + +### Still open + The existing GPT-name comparison and broad/ambiguous partition selection still need hardening. A stable media ID does not authenticate storage or rule out -in-place mutation. WLAN/Bluetooth success remains coupled, and the two FDT -updates remain sequential: a failed second update leaves the first applied. +in-place mutation. WLAN/Bluetooth success remains coupled. In revision 2 the +two FDT updates are sequential and a failed second update leaves the first +applied. The separate follow-up above isolates those updates in a private tree; +it does not change the callback's address-source or compatible-node policy. Valid existing addresses, exact compatible-node selection, actual factory record semantics and the installed Stubble/provider path need independent qualification. The host adapter tests intentionally reproduce the partial-FDT -failure instead of claiming transactional publication. +failure in the original callback; the separate staged-entry tests verify its +failure-isolation boundary. The sole current GPT-name caller passes the fixed literal `DPP`; the pinned native comparison reads at most four units of its 36-unit media name. A current @@ -138,13 +183,14 @@ a hardware-ID priority array; the follow-up above corrects its indexed lookup. Actual SMBIOS generation and device selection still require qualification before provider deployment. -The next publication change must establish complete, successful file reads and -validate each FDT against its actual supplied byte extent before staging any -mutation. A header's `totalsize` does not establish accessible buffer capacity. -Use the actual bounded libfdt validator and retain the existing caller-owned -buffer; a failed update must not expose earlier WLAN or Bluetooth mutations. -This source architecture remains work to implement and test, not a guarantee -of the current candidate. +The follow-up establishes complete successful reads and validation against +supplied byte extents before staged mutation. A header's `totalsize` still +does not prove pointer accessibility. Existing `GetVolume()` and `FileOpen()` +collapse native errors into missing handles; the new missing-file flag only +separates the failed-open control flow from post-open I/O errors. Precise +opening-error classification, real cleanup services and actual installed +caller/provider behavior remain unqualified. The revision-2 archive must not +be described as containing the follow-up. No raw-storage capture, provider invocation, boot-order/Secure Boot change or hardware radio test follows from these source checks. Those require separately From a15179897c849e637298886a3d31394d525a6d11 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 18:29:59 +0100 Subject: [PATCH 13/17] Record complete native DTB source revision three Document the immutable five-patch source archive, exact Linux replay and fresh extraction checks. Separate historical reader and matcher evidence from the current staged-publication source, retain deployment limitations and qualify the pre-commit native build inputs. Signed-off-by: Leon Silcott --- .../native-bluetooth-provisioning.md | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/tools/sp11-driver-audit/native-bluetooth-provisioning.md b/tools/sp11-driver-audit/native-bluetooth-provisioning.md index 2f57a0daeb90aa..3ef037197a6db3 100644 --- a/tools/sp11-driver-audit/native-bluetooth-provisioning.md +++ b/tools/sp11-driver-audit/native-bluetooth-provisioning.md @@ -14,7 +14,35 @@ property, byte order or installed provider is correct. Do not add or remove that quirk by guesswork, invent a replacement factory address or start scanning raw storage from a new Linux driver to work around unverified boot integration. -## Native source candidate +## Current complete native source candidate + +The source-only `sp11-native-dpp-source-20261015-r3` bundle contains the complete +five-commit series over [dtbloader](https://github.com/TravMurav/dtbloader) at +`b1bf23e88b779ac323fbcbe547cf3ba939433df3`. It adds sized DTB inputs and staged +publication (`3489cd54a96f1a1e9397894bef101da220eb3e10`) to the four commits +listed below. The resulting tree is +`86123f3325150775539e3ba3ae16f6a69b873b76`. Apply it to that original base, +not on top of the earlier archives. + +The revision-3 archive SHA-256 is +`dea11979cfa914cf12614d37b257dd4cb925f74bd98d62e5c37354fcb98346ab`. +Its 90,397 bytes contain 38 text files: five patch emails, 29 exact source and +licence snapshots, and four metadata files. Two deterministic packs matched +byte-for-byte. Independent fresh Linux extraction checked all 37 checksum +entries and compared all 38 extracted files against the reviewed staging. + +Independent Linux replay of all five emails reproduced the exact tree and +preserved raw author identities/dates, complete messages, parsed trailers and +stable patch IDs. All six public test targets passed after replay. Repository +integrity also passed with the complete 117-commit base ancestry; initial +offline integrity checks lacked that ancestry, and their failures are retained +separately from the accepted result. Replay committer metadata may differ. + +This is buildable research source, not an installable kernel or a qualified +EFI provider. The bundle contains no compiled image, Windows payload, private +capture or provisioning record. Earlier archives remain unchanged. + +## Historical revision-2 reader and matcher series The source-only `sp11-native-dpp-source-20261015-r2` bundle applies four focused commits to [dtbloader](https://github.com/TravMurav/dtbloader) at @@ -54,7 +82,7 @@ not assertions about factory-format maxima. No proprietary format header, Windows binary, recovered pseudocode or real provisioning record is included. The original BSD-3-Clause licence and contributors' attribution are retained. -## What the checks establish +## Historical revision-2 checks The actual standalone core passed 4,691 synthetic calls and 300,856 assertions per optimized and sanitizer host run. The actual EFI adapter/Surface wrapper, @@ -121,12 +149,11 @@ separate migration decision. Current source checks preserve the legacy ABI. ## Remaining deployment gates -### Separate follow-up: sized inputs and staged publication +### Revision-3 sized inputs and staged publication -Local native commit `3489cd54a96f1a1e9397894bef101da220eb3e10` adds the sized -input/staging prerequisite on a separate branch over the four-commit source -above. Its tree is `86123f3325150775539e3ba3ae16f6a69b873b76`. It is not yet -included in the immutable revision-2 source archive or a firmware release. +Native commit `3489cd54a96f1a1e9397894bef101da220eb3e10` adds the sized +input/staging prerequisite over the four-commit source above. It is included +in revision 3, not retroactively added to revision 2 or a firmware release. The file path now checks a bounded metadata query and exact read, validates against the bytes actually read and closes file/volume handles. A shared @@ -156,8 +183,11 @@ standalone core/matcher suites still pass with unchanged test dependencies. Separate private actual-source fixtures additionally cover loader lifecycle with 52 scenarios and the historical partial-mutation negative control. These do not qualify actual filesystem opening, SHA/NVRAM authorization or factory -provisioning. The full native 86-object ARM64 COFF link passed without warnings; -retained source/object/image bytes were independently audited. OpenCode's +provisioning. The full native 86-object ARM64 COFF link passed without warnings +from a pre-commit snapshot whose compiled inputs match the final commit; +retained source/object/image bytes were independently audited. The historical +87-scenario adapter fixture predates the utility-header change; it was not +rerun against revision 3 and is not a whole-revision regression claim. OpenCode's direct GLM 5.1 supplied-source review found no introduced blocker. Its review and these tests are not proof of firmware ABI, deployment or hardware parity. @@ -167,12 +197,12 @@ The existing GPT-name comparison and broad/ambiguous partition selection still need hardening. A stable media ID does not authenticate storage or rule out in-place mutation. WLAN/Bluetooth success remains coupled. In revision 2 the two FDT updates are sequential and a failed second update leaves the first -applied. The separate follow-up above isolates those updates in a private tree; +applied. Revision 3 isolates those updates in a private tree; it does not change the callback's address-source or compatible-node policy. Valid existing addresses, exact compatible-node selection, actual factory record semantics and the installed Stubble/provider path need independent qualification. The host adapter tests intentionally reproduce the partial-FDT -failure in the original callback; the separate staged-entry tests verify its +failure in the original callback; the revision-3 staged-entry tests verify its failure-isolation boundary. The sole current GPT-name caller passes the fixed literal `DPP`; the pinned @@ -183,14 +213,14 @@ a hardware-ID priority array; the follow-up above corrects its indexed lookup. Actual SMBIOS generation and device selection still require qualification before provider deployment. -The follow-up establishes complete successful reads and validation against +Revision 3 establishes complete successful reads and validation against supplied byte extents before staged mutation. A header's `totalsize` still does not prove pointer accessibility. Existing `GetVolume()` and `FileOpen()` collapse native errors into missing handles; the new missing-file flag only separates the failed-open control flow from post-open I/O errors. Precise opening-error classification, real cleanup services and actual installed caller/provider behavior remain unqualified. The revision-2 archive must not -be described as containing the follow-up. +be described as containing revision-3 changes. No raw-storage capture, provider invocation, boot-order/Secure Boot change or hardware radio test follows from these source checks. Those require separately From abed22324869737de3f7075ba503aecd7fd90c8e Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 15 Oct 2026 18:48:56 +0100 Subject: [PATCH 14/17] Record native DTB opening-status source qualification Document the separate unarchived opening-status follow-up, portable actual-source loader tests, full native link and supplied-source review. Preserve the immutable revision-three artifact, historical ADR context and explicit firmware and hardware qualification gates. Signed-off-by: Leon Silcott --- .../native-bluetooth-provisioning.md | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/tools/sp11-driver-audit/native-bluetooth-provisioning.md b/tools/sp11-driver-audit/native-bluetooth-provisioning.md index 3ef037197a6db3..4fec1090b9b15c 100644 --- a/tools/sp11-driver-audit/native-bluetooth-provisioning.md +++ b/tools/sp11-driver-audit/native-bluetooth-provisioning.md @@ -191,6 +191,42 @@ rerun against revision 3 and is not a whole-revision regression claim. OpenCode' direct GLM 5.1 supplied-source review found no introduced blocker. Its review and these tests are not proof of firmware ABI, deployment or hardware parity. +### Subsequent file-opening source candidate + +Local native commit `1c7b8f75bf6be7efceeb7aea4b8e32fa0b0cf553`, tree +`f5acac88077dcc3c7da01bb64cc4a6632dc68eb5`, is a separate follow-up over +revision 3. It is not contained in that immutable source archive. + +`GetVolume()` and `FileOpen()` now return explicit native status and a separate +owned handle. Only exact successful opens publish a non-NULL output. Native +errors propagate; unexpected warnings and success without a required interface +or handle fail closed. Failed-service output pointers are not trusted or closed +as though they were owned. Borrowed protocol interfaces remain borrowed. + +The six-path DTB search continues only on a file-open `EFI_NOT_FOUND`, retaining +the original order. Device/access/media failures stop immediately instead of +falling through to a lower-priority file or protocol-only success. Only genuine +exhausted-path absence is optional; lookup, metadata, read and close errors +remain fatal even when their status is also `EFI_NOT_FOUND`. Root-close failure +overrides optional absence. Existing cleanup precedence, normalized-hash order, +staged publication and the legacy protocol interface remain unchanged. + +All eight public targets passed on ARM64 macOS and native ARM64 Linux, including +optimized and sanitizer variants. The helper suite now has 2,969 calls; the new +actual-source loader suite has 142 scenarios. It compiles actual GNU-EFI UTF-16 +functions and libfdt rather than rewriting the search or tree algorithms. +Eighteen loader extraction controls and two behavioral mutants reject malformed +boundaries and reintroduced error masking. The full current-commit 86-object +native COFF compile/link passed without warnings. OpenCode's direct GLM 5.1 +production-source review found no introduced blocker; tests and dependencies +were outside that review's scope and were checked separately. + +The new native ADR `0002-explicit-file-opening-status.md` supersedes only the +opening-status limitation in ADR 0001, leaving the earlier record unchanged. +Real EFI services, handle release, SHA/NVRAM authorization and installed +caller/provider behavior remain unqualified. No EFI image was executed or +installed; these shared-loader changes also need non-SP11 firmware testing. + ### Still open The existing GPT-name comparison and broad/ambiguous partition selection still @@ -215,12 +251,12 @@ before provider deployment. Revision 3 establishes complete successful reads and validation against supplied byte extents before staged mutation. A header's `totalsize` still -does not prove pointer accessibility. Existing `GetVolume()` and `FileOpen()` -collapse native errors into missing handles; the new missing-file flag only -separates the failed-open control flow from post-open I/O errors. Precise -opening-error classification, real cleanup services and actual installed -caller/provider behavior remain unqualified. The revision-2 archive must not -be described as containing revision-3 changes. +does not prove pointer accessibility. Its `GetVolume()` and `FileOpen()` still +collapse native errors into missing handles; the missing-file flag separates +failed-open control flow from post-open I/O errors only. The subsequent source +candidate above corrects that classification, not real filesystem/service +qualification. Neither revision-2 nor revision-3 archives contain that new +candidate; revision 2 also lacks revision-3 staged publication. No raw-storage capture, provider invocation, boot-order/Secure Boot change or hardware radio test follows from these source checks. Those require separately From 91aeba8c32034c39103a4db3945e39ec513c5343 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sat, 17 Oct 2026 09:54:05 +0100 Subject: [PATCH 15/17] docs: record experimental USB4 R5 module validation Signed-off-by: Leon Silcott --- tools/sp11-driver-audit/README.md | 5 + .../native-usb4-r5-validation.md | 132 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 tools/sp11-driver-audit/native-usb4-r5-validation.md diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index 269cd610240fe4..42bea6ee622c63 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -146,6 +146,11 @@ records a separate source-only dtbloader candidate, its exact native build and synthetic coverage, and the provider/boot-chain gates that remain. It does not add a kernel raw-partition reader or change the running device's factory identity. +The [dated USB4 R5 validation supplement](native-usb4-r5-validation.md) records +the source-only firmware-parser correction, later native module/MODPOST proof, +and event-channel requirements identified by static analysis. It does not +qualify USB4 activation or provide modules for the running v26 kernel. + The research checkout began at the older integration revision `2cbd1ec3e2da385e7bd91fd65c63ba5a8fb5b865`. These tools are independent of that kernel revision and are fork research infrastructure. Hardware changes must diff --git a/tools/sp11-driver-audit/native-usb4-r5-validation.md b/tools/sp11-driver-audit/native-usb4-r5-validation.md new file mode 100644 index 00000000000000..dc2fe11a5efeb7 --- /dev/null +++ b/tools/sp11-driver-audit/native-usb4-r5-validation.md @@ -0,0 +1,132 @@ +# Native USB4 source R5 — validation supplement, 17 October 2026 + +This is experimental source validation, **not a working USB4 release**. Do not +load or activate the Qualcomm host-router frontend. Its explicit DT/sysfs +activation paths remain reachable; the prohibition is operational, not a +compile-time interlock. This work does not change the accepted v25/v26 kernels. + +This dated note supplements the immutable R5 source archive. The archive's +README correctly records the object-only evidence available when it was +created; the subsequent module checks below do not rewrite that history. + +## Source identity and delivery + +The complete base is `e02a1a495958175c63c65643c4cc8ab80647330f` from +[the kernel fork](https://github.com/ooaklee/linux_ms_dev_kit-sp11). +The local corrected tip is `bae6318b004835cfcbf60fff75e80f1902d5c315`, tree +`5c525eaf0627d3cd1af9d8e93bac684932765c8f`. The local tip is not claimed to be +publicly retrievable. Seven included email patches reconstruct its six changed +files from the exact base. This is not a standalone or DKMS driver. + +The source-only archive is `sp11-native-usb4-source-20261017-r5.tar.gz`, +88,024 bytes, SHA-256: + +```text +e24c557cd5bc3f02288141ee2309f1b1066ca881630f60b0b59cf62cebfca94b +``` + +It contains six complete changed source files, seven email patches, both base +licence texts, a README, manifest and checksum list. The first six emails and +licences remain byte-identical to R4. A minimal six-file replay preserved author +metadata, raw dates, complete messages and stable patch IDs for all seven commits +and matched the final snapshots. That is not a full-tree replay claim. An +independent in-memory archive reconstruction matched every byte. + +No Windows executable, firmware payload, recovered pseudocode, captured table, +private log, compiled module or signing key is included. Compiling this +experimental source does not require Windows or the MSI. Firmware sourcing, +authenticity and the physical activation protocol remain unresolved requirements. + +## Correction and completed checks + +R5 validates the entire cold-load firmware container before that branch halts +the UC or writes payloads. It checks complete headers and payloads using +remaining-byte arithmetic, retains destination/count limits, rejects empty or +trailing incomplete input and unaligned destinations, and reads little-endian +words without assuming input alignment. Record order and overlaps are retained. +Earlier resource preparation and register reads are outside this guarantee. + +The exact parser/bring-up functions passed 90 host cases and 130,436 assertions +in each optimized and ASan/UBSan mode, with ten named inverse controls rejected. +Full ordered reference comparison covers aligned inputs; other byte alignments +have payload/counter oracles. One observed sample passed the pure validator in +memory without MMIO. These do not authenticate firmware or define all vendor +formats. + +The later native ARM64 check freshly compiled `qcom_usb4_hr.o` with GCC 13.3.0, +binutils 2.42 and `W=1`, ran normal in-tree MODPOST, and linked both unsigned +modules. No new-module diagnostics were captured. OpenCode reviewed the +prepared procedure; the completed evidence was separately inspected and audited. + +The configuration was unchanged `ubuntu_x1e_defconfig`, SHA-256: + +```text +a3cdca096dabe73dc9c2b060722d1744733e42bf808a7e0c3edd3dca4363acec +``` + +This is the prototype release **`7.2.0`**, not the accepted v26 `7.2.5` ABI. +Built-in objects were reused, not freshly rebuilt: all 4,196 object hashes and +current GENKSYMS command hashes were checked before/after. The original baseline +did not individually pin those command bytes. All 15,895 regenerated five-field +symbol rows matched its captured table after MODPOST; that captured table was +never imported or copied as a build input. The original built-in pass's 1,148 +diagnostics remain historical evidence, not a warning-free whole-kernel claim. + +Both final AArch64 ELF modules passed basic and extended version checks, strong +import/provider resolution, namespace/GPL checks and dependency comparison. +`nhi_remove`'s final export metadata matches CRC `0x8aa954cf`, GPL flag 1 and +empty namespace. This narrow check does not establish relocation correctness or +module loadability. + +- `qcom_usb4_hr.ko`: 61,144 bytes, 69 versioned imports, dependency `thunderbolt`. + Compared with R4, only `_dev_err` was removed; no imports were added. +- `thunderbolt.ko`: 868,344 bytes, 234 versioned imports; unchanged module bytes. + +No signing, installation, loading, device activation or hardware test occurred. +The temporary private key in the original build cache was neither opened nor +copied into the new output. The modules remain private validation artifacts, +not an installation bundle. + +## Reproduce a build only + +Obtain a disposable complete checkout of the exact base and apply all seven +archive patches in order, following its checksum/application instructions. +On native ARM64 Linux with the normal kernel prerequisites: + +```sh +make O=../usb4-r5-check ARCH=arm64 ubuntu_x1e_defconfig +sha256sum ../usb4-r5-check/.config +make O=../usb4-r5-check ARCH=arm64 W=1 -j2 vmlinux_o +make O=../usb4-r5-check ARCH=arm64 W=1 V=1 -j2 \ + drivers/thunderbolt/thunderbolt.ko drivers/thunderbolt/qcom_usb4_hr.ko +``` + +Compare the configuration digest with the one above before continuing. A +different compiler or prerequisite set can change it; stop and review that +delta instead of claiming identical validation. A fresh built-in build can +generate default temporary signing material: keep it private and out of any +archive. Do not import another kernel's `Module.symvers` or use suppression +flags to bypass module checks. These commands do not install the results, and +the results must not be loaded into an unrelated installed kernel. + +## Next functional work + +Static investigation establishes a missing event-channel contract, not a safe +register sequence to copy. A running UC does not by itself establish a valid +event-buffer location, bounded layout or interrupt readiness. Native work must +validate resource-relative offsets, entry/header extents, producer/consumer +indices and lifetimes, including a fresh-instance warm start. + +The inspected Windows path can publish event state and continue after its +positive timeout result. Linux should retain negative timeout failure instead +of adopting that behavior. The inspected PCIe-interrupt selector is mutable +software policy, not board identity; native support must not depend on Windows +registry values. Decompiler output alone incorrectly suggested a constant +selector, which the actual instructions and output-pointer calls contradict. + +Physical resource/MMIO ownership, IRQ acknowledgement and masking, event-memory +coherency, DMA/work quiescence, powered dependencies, stop/idle/restart and +recovery still require implementation and qualification. The inherited arming +sequence still contains unaligned word accesses; guessed address substitutions +are not a justified repair. Suspend/resume, working USB4 and full Windows/Linux +feature parity remain unproven. From 0173f47bdbd7b5de8250604308d9792e751ad983 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sat, 17 Oct 2026 10:18:38 +0100 Subject: [PATCH 16/17] docs: record native USB4 resource-bounds validation Document the source-only companion correction, host boundary tests and fresh ARM64 object evidence without extending the earlier module proof or claiming hardware readiness. Keep the older source archive and dated validation history intact. Signed-off-by: Leon Silcott --- tools/sp11-driver-audit/README.md | 5 + .../native-usb4-resource-bounds.md | 121 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 tools/sp11-driver-audit/native-usb4-resource-bounds.md diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index 42bea6ee622c63..40b5632beadc8c 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -151,6 +151,11 @@ the source-only firmware-parser correction, later native module/MODPOST proof, and event-channel requirements identified by static analysis. It does not qualify USB4 activation or provide modules for the running v26 kernel. +The subsequent [USB4 resource-bounds correction](native-usb4-resource-bounds.md) +adds validation of the existing frontend's MMIO extents, source-linked host +tests and a fresh ARM64 object check. Its source-only companion patch does not +inherit R5's module proof or establish a working firmware event channel. + The research checkout began at the older integration revision `2cbd1ec3e2da385e7bd91fd65c63ba5a8fb5b865`. These tools are independent of that kernel revision and are fork research infrastructure. Hardware changes must diff --git a/tools/sp11-driver-audit/native-usb4-resource-bounds.md b/tools/sp11-driver-audit/native-usb4-resource-bounds.md new file mode 100644 index 00000000000000..bd14cbb5299efb --- /dev/null +++ b/tools/sp11-driver-audit/native-usb4-resource-bounds.md @@ -0,0 +1,121 @@ +# Native USB4 resource bounds — 17 October 2026 + +This is a source-only correction to the experimental Qualcomm host-router +frontend, **not working or hardware-qualified USB4 support**. Do not load or +activate it. Existing explicit DT/sysfs activation paths remain reachable; +this warning is not a compile-time interlock. Accepted v25/v26 sources and the +Surface installation are unchanged. + +## Correction and source identity + +The local source commit is `7620268bebf4c7984691707e32effbd0778b25bf`, following +R5 commit `bae6318b004835cfcbf60fff75e80f1902d5c315`. Only +`drivers/thunderbolt/qcom_usb4_hr.c` changes. Its SHA-256 is: + +```text +a505501e30dfe5b22c7cf3bfd2fa5f1d57d327055a302b6c81d281ec369c8ae8 +``` + +The firmware parser limits destinations to 56 KiB, but the old probe accepted +a successfully mapped UC RAM resource smaller than that. Other register and +diagnostic accesses also assumed minimum extents. Probe now validates every +named range before mapping any of them or entering power/PHY/firmware setup. +Missing, reversed, inclusive-size-wrapped and undersized ranges fail. +Mapping errors retain their error codes and device-managed cleanup. + +The nonzero minimum extents cover the existing frontend accesses: + +| Named resource | Minimum bytes | +| --- | ---: | +| `router` | `0x20ac` | +| `router_config` | `0x1c` | +| `port_group` | `0x68` | +| `sideband` | `0x40` | +| `uc_ram` | `0xe000` | +| `uc_per` | `0x1004` | +| `usbap_config` | `0x18` | +| `nhi` | `0x40000` | + +The NHI value conservatively retains the complete described Qualcomm aperture; +it is not a derived minimum for every NHI implementation. Unused named ranges +remain required with a representable nonzero size. Larger UC RAM resources +do not enlarge the firmware parser's fixed destination limit. The complete +firmware validator and bring-up functions remain byte-identical to R5. + +Logical bounds do not prove physical ownership, powered access or alignment +safety. In particular, this does not repair or authorize the inherited +optional unaligned interrupt-arming accesses or raw PHY/mailbox experiments. + +## Validation actually completed + +The exact production resource declaration, table and mapping helper were +extracted into a host fixture. Each optimized and ASan/UBSan run passed 97 +named cases and 6,703 assertions. Tests cover names/order, exact and short +extents, larger resources, missing and reversed ranges, full-width wrap, +mapping failures and validation before any map. Removing the minimum-size +guard failed the independent UC RAM extent oracle in both modes. + +The actual probe's early error-return ordering was checked statically, not +executed as a complete platform lifecycle. The fixture does not simulate +physical MMIO or device-managed cleanup. Earlier firmware tests were not +rerun or presented as new whole-driver coverage. + +A fresh native ARM64 `W=1` object compilation passed with GCC 13.3.0 and +binutils 2.42, using unchanged `ubuntu_x1e_defconfig`: + +```text +a3cdca096dabe73dc9c2b060722d1744733e42bf808a7e0c3edd3dca4363acec +``` + +The 56,832-byte AArch64 ELF relocatable object has SHA-256: + +```text +d36db78b50f6222fb36aaad21283ff880c0e73d2befb7c12b479b3db42ae905e +``` + +The compiler ran non-root with fresh output and read-only source. The full +source carrier and six overlays were checked before/after; no compiler +diagnostics were captured. OpenCode reviewed the source and prepared procedure; +completed artifacts received a separate read-only audit. + +This is the prototype **7.2.0** configuration, not the installed v26 7.2.5 ABI. +The new object adds `devm_ioremap_resource` and +`platform_get_resource_byname`, replacing +`devm_platform_ioremap_resource_byname`. These are undefined object symbols, +not newly verified module imports. R5's later +[module validation](native-usb4-r5-validation.md) does not apply automatically +to this changed object. New MODPOST, module linking, packaging, loadability +and hardware qualification remain outstanding. + +## Source-only companion delivery + +The immutable R5 archive remains unchanged. Apply the companion email patch +after its seven patches in a disposable complete checkout of the exact base: + +`sp11-native-usb4-resource-bounds-20261017.patch` — 4,834 bytes, SHA-256: + +```text +b211dd75be346927bb0bc3b9ab2e64f86d7ef5f75f28d15efb50d19effd8b0cd +``` + +A matching `.patch.sha256` sidecar is supplied. The patch was applied to a +fresh minimal repository containing all six exact R5 snapshots. All six final +files, author/raw date/complete message and stable patch ID matched the new +source. That is a minimal source replay, not a full-tree replay. The local +commit is not claimed publicly retrievable, and this note does not publish +the patch or archive remotely. + +This is not a standalone or DKMS driver. Use the complete exact kernel base +and matching build prerequisites; no Windows files are needed for compilation. +No Windows binaries, firmware, recovered pseudocode, private captures, +compiled modules or signing keys are part of the companion patch. Firmware +sourcing/authenticity remains a separate unresolved dependency. + +## Next functional requirement + +Static analysis now distinguishes the configuration command from event-ring +initialization and traces notification scheduling. The observed counter lock +does not establish that firmware ring entries remain stable while consumed. +Linux still needs a verified producer/consumer and acknowledgement contract, +resource ownership, safe warm-start reconstruction and physical stop/idle. +Do not infer USB4, dock, standby or complete feature parity from these checks. From 67d009fac1c15cb1806c693a5be555d119595780 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 10 Nov 2026 14:25:55 +0000 Subject: [PATCH 17/17] tools: document 7.3 research scope and test parsers in CI --- .github/workflows/sp11-driver-audit.yml | 24 ++++++++++++++++++++++++ tools/sp11-driver-audit/README.md | 6 ++++++ 2 files changed, 30 insertions(+) create mode 100644 .github/workflows/sp11-driver-audit.yml diff --git a/.github/workflows/sp11-driver-audit.yml b/.github/workflows/sp11-driver-audit.yml new file mode 100644 index 00000000000000..da5000912658f2 --- /dev/null +++ b/.github/workflows/sp11-driver-audit.yml @@ -0,0 +1,24 @@ +name: SP11 driver audit tools + +on: + pull_request: + paths: + - tools/sp11-driver-audit/** + - .github/workflows/sp11-driver-audit.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + parser-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: '22' + - name: Test bounded parsers and command-line behavior + run: node --test tools/sp11-driver-audit/*.test.mjs diff --git a/tools/sp11-driver-audit/README.md b/tools/sp11-driver-audit/README.md index 40b5632beadc8c..cf8aa89bce3e03 100644 --- a/tools/sp11-driver-audit/README.md +++ b/tools/sp11-driver-audit/README.md @@ -1,5 +1,11 @@ # Surface Pro 11 driver research +This 7.3 integration imports research tools and historical validation notes. +The notes retain their original 7.2 source identities and evidence limits; they +do not describe a new build or installation of the current kernel. Native +OV13858 and VD55 camera code is tracked separately in +[PR #73](https://github.com/ooaklee/linux_ms_dev_kit-sp11/pull/73). + These independently written Linux development tools inventory an extracted Surface driver package and decode selected camera resource metadata. They are the first source deliverable in the SP11 parity work. They do not install