@svrnsec/pulse 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * @sovereign/pulse — WebGPU Thermal Variance Probe
3
+ *
4
+ * Runs a compute shader on the GPU and measures dispatch timing variance.
5
+ *
6
+ * Why this works
7
+ * ──────────────
8
+ * Real consumer GPUs (GTX 1650, RX 6600, M2 GPU) have thermal noise in shader
9
+ * execution timing that increases under sustained load — the same thermodynamic
10
+ * principle as the CPU probe but in silicon designed for parallel throughput.
11
+ *
12
+ * Cloud VMs with software GPU emulation (SwiftShader, llvmpipe, Mesa's softpipe)
13
+ * execute shaders on the CPU and produce near-deterministic timing — flat CV,
14
+ * no thermal growth across phases, no dispatch jitter.
15
+ *
16
+ * VMs with GPU passthrough (rare in practice, requires dedicated hardware) pass
17
+ * this check — which is correct, they have real GPU silicon.
18
+ *
19
+ * Signals
20
+ * ───────
21
+ * gpuPresent false = WebGPU absent = software renderer = high VM probability
22
+ * isSoftware true = SwiftShader/llvmpipe detected by adapter info
23
+ * dispatchCV coefficient of variation across dispatch timings
24
+ * thermalGrowth (hotDispatchMean - coldDispatchMean) / coldDispatchMean
25
+ * vendorString GPU vendor from adapter info (Intel, NVIDIA, AMD, Apple, etc.)
26
+ */
27
+
28
+ /* ─── WebGPU availability ────────────────────────────────────────────────── */
29
+
30
+ function isWebGPUAvailable() {
31
+ return typeof navigator !== 'undefined' && 'gpu' in navigator;
32
+ }
33
+
34
+ /* ─── Software renderer detection ───────────────────────────────────────── */
35
+
36
+ const SOFTWARE_RENDERER_PATTERNS = [
37
+ /swiftshader/i,
38
+ /llvmpipe/i,
39
+ /softpipe/i,
40
+ /microsoft basic render/i,
41
+ /angle \(.*software/i,
42
+ /cpu/i,
43
+ ];
44
+
45
+ function detectSoftwareRenderer(adapterInfo) {
46
+ const desc = [
47
+ adapterInfo?.vendor ?? '',
48
+ adapterInfo?.device ?? '',
49
+ adapterInfo?.description ?? '',
50
+ adapterInfo?.architecture ?? '',
51
+ ].join(' ');
52
+
53
+ return SOFTWARE_RENDERER_PATTERNS.some(p => p.test(desc));
54
+ }
55
+
56
+ /* ─── Compute shader ─────────────────────────────────────────────────────── */
57
+
58
+ // A compute workload that is trivially parallelisable but forces the GPU to
59
+ // actually execute — matrix-multiply on 64 × 64 tiles across 256 workgroups.
60
+ // Light enough that it doesn't block UI; heavy enough to generate thermal signal.
61
+ const SHADER_SRC = /* wgsl */ `
62
+ struct Matrix {
63
+ values: array<f32, 4096>, // 64x64
64
+ };
65
+
66
+ @group(0) @binding(0) var<storage, read> matA : Matrix;
67
+ @group(0) @binding(1) var<storage, read> matB : Matrix;
68
+ @group(0) @binding(2) var<storage, read_write> matC : Matrix;
69
+
70
+ @compute @workgroup_size(8, 8)
71
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
72
+ let row = gid.x;
73
+ let col = gid.y;
74
+ if (row >= 64u || col >= 64u) { return; }
75
+
76
+ var acc: f32 = 0.0;
77
+ for (var k = 0u; k < 64u; k++) {
78
+ acc += matA.values[row * 64u + k] * matB.values[k * 64u + col];
79
+ }
80
+ matC.values[row * 64u + col] = acc;
81
+ }
82
+ `;
83
+
84
+ /* ─── collectGpuEntropy ─────────────────────────────────────────────────── */
85
+
86
+ /**
87
+ * @param {object} [opts]
88
+ * @param {number} [opts.iterations=60] – dispatch rounds per phase
89
+ * @param {boolean} [opts.phased=true] – cold / load / hot phases
90
+ * @param {number} [opts.timeoutMs=8000] – hard abort if GPU stalls
91
+ * @returns {Promise<GpuEntropyResult>}
92
+ */
93
+ export async function collectGpuEntropy(opts = {}) {
94
+ const { iterations = 60, phased = true, timeoutMs = 8000 } = opts;
95
+
96
+ if (!isWebGPUAvailable()) {
97
+ return _noGpu('WebGPU not available in this environment');
98
+ }
99
+
100
+ let adapter, device;
101
+ try {
102
+ adapter = await Promise.race([
103
+ navigator.gpu.requestAdapter({ powerPreference: 'high-performance' }),
104
+ _timeout(timeoutMs, 'requestAdapter timed out'),
105
+ ]);
106
+ if (!adapter) return _noGpu('No WebGPU adapter found');
107
+
108
+ device = await Promise.race([
109
+ adapter.requestDevice(),
110
+ _timeout(timeoutMs, 'requestDevice timed out'),
111
+ ]);
112
+ } catch (err) {
113
+ return _noGpu(`WebGPU init failed: ${err.message}`);
114
+ }
115
+
116
+ const adapterInfo = adapter.info ?? {};
117
+ const isSoftware = detectSoftwareRenderer(adapterInfo);
118
+
119
+ // Compile the shader module once
120
+ const shaderModule = device.createShaderModule({ code: SHADER_SRC });
121
+
122
+ // Create persistent GPU buffers (64×64 float32 = 16 KB each)
123
+ const bufSize = 4096 * 4; // 4096 floats × 4 bytes
124
+ const bufA = _createBuffer(device, bufSize, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
125
+ const bufB = _createBuffer(device, bufSize, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
126
+ const bufC = _createBuffer(device, bufSize, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
127
+
128
+ // Seed with random data
129
+ const matData = new Float32Array(4096).map(() => Math.random());
130
+ device.queue.writeBuffer(bufA, 0, matData);
131
+ device.queue.writeBuffer(bufB, 0, matData);
132
+
133
+ const pipeline = device.createComputePipeline({
134
+ layout: 'auto',
135
+ compute: { module: shaderModule, entryPoint: 'main' },
136
+ });
137
+
138
+ const bindGroup = device.createBindGroup({
139
+ layout: pipeline.getBindGroupLayout(0),
140
+ entries: [
141
+ { binding: 0, resource: { buffer: bufA } },
142
+ { binding: 1, resource: { buffer: bufB } },
143
+ { binding: 2, resource: { buffer: bufC } },
144
+ ],
145
+ });
146
+
147
+ // ── Probe ──────────────────────────────────────────────────────────────
148
+ async function runPhase(n) {
149
+ const timings = [];
150
+ for (let i = 0; i < n; i++) {
151
+ const t0 = performance.now();
152
+ const encoder = device.createCommandEncoder();
153
+ const pass = encoder.beginComputePass();
154
+ pass.setPipeline(pipeline);
155
+ pass.setBindGroup(0, bindGroup);
156
+ pass.dispatchWorkgroups(8, 8); // 64 workgroups total
157
+ pass.end();
158
+ device.queue.submit([encoder.finish()]);
159
+ await device.queue.onSubmittedWorkDone();
160
+ const t1 = performance.now();
161
+ timings.push(t1 - t0);
162
+ }
163
+ return timings;
164
+ }
165
+
166
+ let coldTimings, loadTimings, hotTimings;
167
+
168
+ if (phased) {
169
+ coldTimings = await runPhase(Math.floor(iterations * 0.25));
170
+ loadTimings = await runPhase(Math.floor(iterations * 0.50));
171
+ hotTimings = await runPhase(iterations - coldTimings.length - loadTimings.length);
172
+ } else {
173
+ coldTimings = await runPhase(iterations);
174
+ loadTimings = [];
175
+ hotTimings = [];
176
+ }
177
+
178
+ // Cleanup
179
+ bufA.destroy(); bufB.destroy(); bufC.destroy();
180
+ device.destroy();
181
+
182
+ const allTimings = [...coldTimings, ...loadTimings, ...hotTimings];
183
+ const mean = _mean(allTimings);
184
+ const cv = mean > 0 ? _std(allTimings) / mean : 0;
185
+
186
+ const coldMean = _mean(coldTimings);
187
+ const hotMean = _mean(hotTimings.length ? hotTimings : coldTimings);
188
+ const thermalGrowth = coldMean > 0 ? (hotMean - coldMean) / coldMean : 0;
189
+
190
+ return {
191
+ gpuPresent: true,
192
+ isSoftware,
193
+ vendor: adapterInfo.vendor ?? 'unknown',
194
+ architecture: adapterInfo.architecture ?? 'unknown',
195
+ timings: allTimings,
196
+ dispatchCV: cv,
197
+ thermalGrowth,
198
+ coldMean,
199
+ hotMean,
200
+ // Heuristic: real GPU → thermalGrowth > 0.02 and CV > 0.04
201
+ // Software renderer → thermalGrowth ≈ 0, CV < 0.02
202
+ verdict: isSoftware ? 'software_renderer'
203
+ : thermalGrowth > 0.02 && cv > 0.04 ? 'real_gpu'
204
+ : thermalGrowth < 0 && cv < 0.02 ? 'virtual_gpu'
205
+ : 'ambiguous',
206
+ };
207
+ }
208
+
209
+ /* ─── helpers ────────────────────────────────────────────────────────────── */
210
+
211
+ function _noGpu(reason) {
212
+ return { gpuPresent: false, isSoftware: false, vendor: null,
213
+ architecture: null, timings: [], dispatchCV: 0,
214
+ thermalGrowth: 0, coldMean: 0, hotMean: 0,
215
+ verdict: 'no_gpu', reason };
216
+ }
217
+
218
+ function _createBuffer(device, size, usage) {
219
+ return device.createBuffer({ size, usage });
220
+ }
221
+
222
+ function _mean(arr) {
223
+ return arr.length ? arr.reduce((s, v) => s + v, 0) / arr.length : 0;
224
+ }
225
+
226
+ function _std(arr) {
227
+ const m = _mean(arr);
228
+ return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / arr.length);
229
+ }
230
+
231
+ function _timeout(ms, msg) {
232
+ return new Promise((_, reject) => setTimeout(() => reject(new Error(msg)), ms));
233
+ }
234
+
235
+ /**
236
+ * @typedef {object} GpuEntropyResult
237
+ * @property {boolean} gpuPresent
238
+ * @property {boolean} isSoftware
239
+ * @property {string|null} vendor
240
+ * @property {string|null} architecture
241
+ * @property {number[]} timings
242
+ * @property {number} dispatchCV
243
+ * @property {number} thermalGrowth
244
+ * @property {string} verdict 'real_gpu' | 'virtual_gpu' | 'software_renderer' | 'no_gpu' | 'ambiguous'
245
+ */