The simulation below models the three phases of a Spectre-variant-1 attack. The real attack runs in nanoseconds on bare metal; here the CPU is simulated in JavaScript so you can slow it down and watch each step.
<!-- Three-phase Spectre simulation — train, probe, read -->
<div class="panel" id="train-panel">
<h3>Phase 1 — Train the Branch Predictor</h3>
<p class="desc">Click the button to train the branch predictor. After enough iterations it will always predict 'taken', allowing speculative out-of-bounds access in Phase 2.</p>
<div class="pred-bar-wrap">
<div class="pred-bar" id="pred-bar"></div>
<span class="pred-label" id="pred-label">Predictor: untrained</span>
</div>
<button id="btn-train" type="button">Train branch</button>
</div>
<div class="panel" id="probe-panel">
<h3>Phase 2 — Speculative Probe</h3>
<p class="desc">With a trained predictor, the CPU speculatively executes the highlighted line even though index is out of bounds. The secret byte is encoded into which cache line is touched.</p>
<div class="code-box" id="code-box">
<span class="line"><span class="kw">if</span> (index < array.length) {</span>
<span class="line speculative" id="spec-line"> y = probe[array[index] * 512] <span class="badge">SPECULATIVE</span></span>
<span class="line">}</span>
</div>
<button id="btn-probe" type="button" disabled>Speculative probe</button>
</div>
<div class="panel" id="read-panel">
<h3>Phase 3 — Read the Cache</h3>
<p class="desc">A timing sweep across all 256 probe slots reveals which one is cached (fast, red bar). The fastest slot index is the leaked secret byte.</p>
<div class="timing-wrap">
<canvas id="timing-canvas" width="512" height="100"></canvas>
</div>
<div class="result" id="result"></div>
</div>
/* Spectre demo styles — three panels plus timing canvas */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px 0; }
h3 { margin: 0 0 .3rem; font-size: .95rem; text-transform: uppercase;
letter-spacing: .05em; color: #1d3557; }
.desc { font-size: .82rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.panel { border: 1px solid #cdd9e3; border-radius: 10px; padding: .7rem .9rem;
margin-bottom: .6rem; background: #f8fafc; }
/* Predictor training progress bar */
.pred-bar-wrap { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
.pred-bar { height: 14px; background: #1d3557; border-radius: 7px; width: 0%;
transition: width .25s; flex-shrink: 0; min-width: 2px; max-width: 200px; }
.pred-label { font-size: .8rem; color: #555; }
/* Highlighted pseudo-code showing the speculative load */
.code-box { background: #1e2a38; border-radius: 8px; padding: .5rem .7rem;
font-family: ui-monospace, monospace; font-size: .8rem; color: #a8b9cc;
display: flex; flex-direction: column; gap: 2px; margin-bottom: .5rem; }
.line { display: flex; align-items: center; gap: .4rem; }
.kw { color: #79b8ff; }
.speculative { color: #f0c070; }
.badge { font-size: .65rem; background: #e63946; color: #fff;
border-radius: 4px; padding: 1px 4px; margin-left: .3rem; }
/* Canvas shows per-byte timing; red = fast (cached) */
.timing-wrap { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden;
margin-bottom: .5rem; background: #fff; }
canvas { display: block; }
.result { font-size: .95rem; font-weight: 700; min-height: 1.3em; color: #1d3557; }
.result.found { color: #0a7d33; }
.result.waiting { color: #888; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Spectre variant-1 simulation (conceptual, not a real exploit)
// The CPU and cache are modeled; no actual speculative execution occurs here
// The hidden byte we want to leak (unknown to the victim program)
const SECRET_BYTE = 42;
// Probe array has 256 slots spaced CACHE_LINE bytes apart
const PROBE_SLOTS = 256;
const CACHE_LINE = 512;
// Branch predictor state: counts of training rounds
let trainCount = 0;
const TRAIN_THRESHOLD = 5;
let probeRun = false;
// Simulated timing array — lower value means the slot is cached
// timing[v] = simulated access time for probe slot v (lower = cached)
const timing = new Array(PROBE_SLOTS).fill(200);
const predBar = document.getElementById('pred-bar');
const predLabel = document.getElementById('pred-label');
const btnTrain = document.getElementById('btn-train');
const btnProbe = document.getElementById('btn-probe');
const specLine = document.getElementById('spec-line');
const result = document.getElementById('result');
const canvas = document.getElementById('timing-canvas');
const ctx = canvas.getContext('2d');
function updatePredictor() {
// Update the predictor bar and enable probe when fully trained
const pct = Math.min(100, (trainCount / TRAIN_THRESHOLD) * 100);
predBar.style.width = pct + '%';
if (trainCount === 0) {
predLabel.textContent = 'Predictor: untrained';
} else if (trainCount < TRAIN_THRESHOLD) {
predLabel.textContent = i18nFmt('Predictor: %n% / %t% trains', { n: trainCount, t: TRAIN_THRESHOLD });
} else {
predLabel.textContent = 'Predictor: locked — always taken';
btnProbe.disabled = false;
}
}
function drawTiming() {
// Draw the per-slot timing histogram on the canvas
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const barW = W / PROBE_SLOTS;
for (let v = 0; v < PROBE_SLOTS; v++) {
const t = timing[v];
// Red for hot (cached) slots, gray for cold
const hot = t < 50;
ctx.fillStyle = hot ? '#e63946' : '#c0ccd8';
const barH = Math.min(H - 4, (200 - t) / 200 * (H - 4) + 4);
ctx.fillRect(v * barW, H - barH, barW - 1, barH);
}
}
function doTrain() {
// One training iteration — move the predictor toward always-taken
trainCount++;
updatePredictor();
specLine.style.opacity = '0.4';
setTimeout(() => { specLine.style.opacity = '1'; }, 120);
}
function doProbe() {
// Trigger the speculative probe and animate the timing readout
if (probeRun) return;
probeRun = true;
btnProbe.disabled = true;
result.textContent = 'Scanning cache timing...';
result.className = 'result waiting';
// In the speculative window the CPU reads secret[0] and touches probe[secret * CACHE_LINE]
// Simulate: speculative read encodes SECRET_BYTE into timing[SECRET_BYTE]
timing[SECRET_BYTE] = 8 + Math.floor(Math.random() * 6);
// All other slots have high (noisy) timing values
for (let v = 0; v < PROBE_SLOTS; v++) {
if (v !== SECRET_BYTE) {
timing[v] = 180 + Math.floor(Math.random() * 40);
}
}
// Animate the scan left-to-right for visual clarity
let scanned = 0;
const interval = setInterval(() => {
drawTiming();
scanned += 16;
if (scanned >= PROBE_SLOTS) {
clearInterval(interval);
// Find the minimum timing slot — that is the leaked byte
let minT = Infinity, leaked = -1;
for (let v = 0; v < PROBE_SLOTS; v++) {
if (timing[v] < minT) { minT = timing[v]; leaked = v; }
}
result.textContent = i18nFmt('Leaked byte = %byte% (via cache timing)', { byte: leaked });
result.className = 'result found';
console.log('Leaked byte:', leaked, '| expected:', SECRET_BYTE);
}
}, 40);
}
btnTrain.addEventListener('click', doTrain);
btnProbe.addEventListener('click', doProbe);
updatePredictor();
drawTiming();
<!-- Three-phase Spectre simulation — train, probe, read -->
<div class="panel" id="train-panel">
<h3>Phase 1 — Train the Branch Predictor</h3>
<p class="desc">Click the button to train the branch predictor. After enough iterations it will always predict 'taken', allowing speculative out-of-bounds access in Phase 2.</p>
<div class="pred-bar-wrap">
<div class="pred-bar" id="pred-bar"></div>
<span class="pred-label" id="pred-label">Predictor: untrained</span>
</div>
<button id="btn-train" type="button">Train branch</button>
</div>
<div class="panel" id="probe-panel">
<h3>Phase 2 — Speculative Probe</h3>
<p class="desc">With a trained predictor, the CPU speculatively executes the highlighted line even though index is out of bounds. The secret byte is encoded into which cache line is touched.</p>
<div class="code-box" id="code-box">
<span class="line"><span class="kw">if</span> (index < array.length) {</span>
<span class="line speculative" id="spec-line"> y = probe[array[index] * 512] <span class="badge">SPECULATIVE</span></span>
<span class="line">}</span>
</div>
<button id="btn-probe" type="button" disabled>Speculative probe</button>
</div>
<div class="panel" id="read-panel">
<h3>Phase 3 — Read the Cache</h3>
<p class="desc">A timing sweep across all 256 probe slots reveals which one is cached (fast, red bar). The fastest slot index is the leaked secret byte.</p>
<div class="timing-wrap">
<canvas id="timing-canvas" width="512" height="100"></canvas>
</div>
<div class="result" id="result"></div>
</div>
/* Spectre demo styles — three panels plus timing canvas */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px 0; }
h3 { margin: 0 0 .3rem; font-size: .95rem; text-transform: uppercase;
letter-spacing: .05em; color: #1d3557; }
.desc { font-size: .82rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.panel { border: 1px solid #cdd9e3; border-radius: 10px; padding: .7rem .9rem;
margin-bottom: .6rem; background: #f8fafc; }
/* Predictor training progress bar */
.pred-bar-wrap { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; }
.pred-bar { height: 14px; background: #1d3557; border-radius: 7px; width: 0%;
transition: width .25s; flex-shrink: 0; min-width: 2px; max-width: 200px; }
.pred-label { font-size: .8rem; color: #555; }
/* Highlighted pseudo-code showing the speculative load */
.code-box { background: #1e2a38; border-radius: 8px; padding: .5rem .7rem;
font-family: ui-monospace, monospace; font-size: .8rem; color: #a8b9cc;
display: flex; flex-direction: column; gap: 2px; margin-bottom: .5rem; }
.line { display: flex; align-items: center; gap: .4rem; }
.kw { color: #79b8ff; }
.speculative { color: #f0c070; }
.badge { font-size: .65rem; background: #e63946; color: #fff;
border-radius: 4px; padding: 1px 4px; margin-left: .3rem; }
/* Canvas shows per-byte timing; red = fast (cached) */
.timing-wrap { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden;
margin-bottom: .5rem; background: #fff; }
canvas { display: block; }
.result { font-size: .95rem; font-weight: 700; min-height: 1.3em; color: #1d3557; }
.result.found { color: #0a7d33; }
.result.waiting { color: #888; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Spectre variant-1 simulation (conceptual, not a real exploit)
// The CPU and cache are modeled; no actual speculative execution occurs here
// The hidden byte we want to leak (unknown to the victim program)
const SECRET_BYTE = 42;
// Probe array has 256 slots spaced CACHE_LINE bytes apart
const PROBE_SLOTS = 256;
const CACHE_LINE = 512;
// Branch predictor state: counts of training rounds
let trainCount = 0;
const TRAIN_THRESHOLD = 5;
let probeRun = false;
// Simulated timing array — lower value means the slot is cached
// timing[v] = simulated access time for probe slot v (lower = cached)
const timing = new Array(PROBE_SLOTS).fill(200);
const predBar = document.getElementById('pred-bar');
const predLabel = document.getElementById('pred-label');
const btnTrain = document.getElementById('btn-train');
const btnProbe = document.getElementById('btn-probe');
const specLine = document.getElementById('spec-line');
const result = document.getElementById('result');
const canvas = document.getElementById('timing-canvas');
const ctx = canvas.getContext('2d');
function updatePredictor() {
// Update the predictor bar and enable probe when fully trained
const pct = Math.min(100, (trainCount / TRAIN_THRESHOLD) * 100);
predBar.style.width = pct + '%';
if (trainCount === 0) {
predLabel.textContent = 'Predictor: untrained';
} else if (trainCount < TRAIN_THRESHOLD) {
predLabel.textContent = i18nFmt('Predictor: %n% / %t% trains', { n: trainCount, t: TRAIN_THRESHOLD });
} else {
predLabel.textContent = 'Predictor: locked — always taken';
btnProbe.disabled = false;
}
}
function drawTiming() {
// Draw the per-slot timing histogram on the canvas
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const barW = W / PROBE_SLOTS;
for (let v = 0; v < PROBE_SLOTS; v++) {
const t = timing[v];
// Red for hot (cached) slots, gray for cold
const hot = t < 50;
ctx.fillStyle = hot ? '#e63946' : '#c0ccd8';
const barH = Math.min(H - 4, (200 - t) / 200 * (H - 4) + 4);
ctx.fillRect(v * barW, H - barH, barW - 1, barH);
}
}
function doTrain() {
// One training iteration — move the predictor toward always-taken
trainCount++;
updatePredictor();
specLine.style.opacity = '0.4';
setTimeout(() => { specLine.style.opacity = '1'; }, 120);
}
function doProbe() {
// Trigger the speculative probe and animate the timing readout
if (probeRun) return;
probeRun = true;
btnProbe.disabled = true;
result.textContent = 'Scanning cache timing...';
result.className = 'result waiting';
// In the speculative window the CPU reads secret[0] and touches probe[secret * CACHE_LINE]
// Simulate: speculative read encodes SECRET_BYTE into timing[SECRET_BYTE]
timing[SECRET_BYTE] = 8 + Math.floor(Math.random() * 6);
// All other slots have high (noisy) timing values
for (let v = 0; v < PROBE_SLOTS; v++) {
if (v !== SECRET_BYTE) {
timing[v] = 180 + Math.floor(Math.random() * 40);
}
}
// Animate the scan left-to-right for visual clarity
let scanned = 0;
const interval = setInterval(() => {
drawTiming();
scanned += 16;
if (scanned >= PROBE_SLOTS) {
clearInterval(interval);
// Find the minimum timing slot — that is the leaked byte
let minT = Infinity, leaked = -1;
for (let v = 0; v < PROBE_SLOTS; v++) {
if (timing[v] < minT) { minT = timing[v]; leaked = v; }
}
result.textContent = i18nFmt('Leaked byte = %byte% (via cache timing)', { byte: leaked });
result.className = 'result found';
console.log('Leaked byte:', leaked, '| expected:', SECRET_BYTE);
}
}, 40);
}
btnTrain.addEventListener('click', doTrain);
btnProbe.addEventListener('click', doProbe);
updatePredictor();
drawTiming();
Phase 1 — Train: click Train branch repeatedly. The simulated branch predictor counts "taken" outcomes and eventually locks onto the prediction "always take the branch". This mimics the hundreds of benign iterations an attacker uses to train the predictor.
Phase 2 — Probe: click Speculative probe. The CPU is now mis-directed into speculatively reading secret[0] — memory outside the allowed window. Even though the check fails and the access is rolled back architecturally, the byte's value has been encoded into which cache line is hot.
Phase 3 — Read: the timing panel shows which of the 256 possible byte values produced the fastest cache hit. The fastest slot reveals the secret byte — without ever having the CPU "officially" return it.
Comments
Loading comments...