This demo simulates the core of a Spectre-style attack in three steps. First, a branch is trained so the predictor expects the "safe" path. Then one out-of-bounds access is fired speculatively. Finally, the cache timing reveals which memory line was touched — exposing the secret without ever reading it directly.
<!-- Three-phase Spectre-style demo: Train → Attack → Probe -->
<div class="panel" id="panel-train">
<h3>Phase 1 — Train the Branch Predictor</h3>
<p class="desc">We call victim_fn(i) for i = 0…9, always within bounds. The branch predictor learns: index < array.length → always true → take the safe path.</p>
<div class="progress-row">
<span class="label">Training iteration:</span>
<div class="bar-wrap"><div class="bar" id="bar-train"></div></div>
<span class="count" id="count-train">0 / 10</span>
</div>
<div class="branch-vis" id="branch-vis">
<span class="branch-true" id="bvis-true">✓ safe path</span>
<span class="branch-sep">|</span>
<span class="branch-false" id="bvis-false">✗ out of bounds</span>
</div>
<button id="btn-train" type="button">Start Training</button>
</div>
<div class="panel" id="panel-attack" style="display:none">
<h3>Phase 2 — Fire the Malicious Access</h3>
<p class="desc">Now we call victim_fn(secret_offset) where secret_offset points outside the array. The predictor — still confident — speculatively executes the safe path, reading array[secret_offset] and warming a cache line. The CPU then detects the bounds violation and squashes the result. The value never reaches a register, but the cache remembers.</p>
<div class="secret-row">
<span class="label">Secret value:</span>
<span class="secret-box" id="secret-display">?</span>
<span class="label small">(hidden from program; only the cache knows)</span>
</div>
<button id="btn-attack" type="button">Fire Malicious Access</button>
</div>
<div class="panel" id="panel-probe" style="display:none">
<h3>Phase 3 — Read the Cache (Timing Probe)</h3>
<p class="desc">We time access to each probe slot. The hot slot (fast access) reveals which cache line was touched during speculation — exposing the secret value.</p>
<div id="probe-grid" class="probe-grid"></div>
<div class="status" id="status-probe"></div>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for the three-phase Spectre demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; }
.panel { background: #f4f7fa; border: 1px solid #d0dae4; border-radius: 10px;
padding: 1rem 1.1rem; margin-bottom: .8rem; }
h3 { font-size: 1rem; font-weight: 700; margin-bottom: .4rem; color: #1d3557; }
.desc { font-size: .88rem; color: #444; line-height: 1.5; margin-bottom: .8rem; }
/* Training progress bar */
.progress-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; }
.label { font-size: .82rem; color: #555; white-space: nowrap; }
.label.small { font-size: .78rem; color: #777; }
.bar-wrap { flex: 1; height: 10px; background: #d5dfe8; border-radius: 5px; overflow: hidden; }
.bar { height: 100%; width: 0; background: #457b9d; border-radius: 5px; transition: width .12s; }
.count { font-size: .82rem; color: #555; white-space: nowrap; min-width: 3rem; text-align: right; }
/* Branch predictor visualizer */
.branch-vis { display: flex; align-items: center; gap: .5rem; margin-bottom: .8rem;
font-size: .85rem; font-weight: 600; }
.branch-true { color: #0a7d33; padding: .25rem .55rem; border-radius: 6px; background: #d4f0dc; }
.branch-false { color: #888; padding: .25rem .55rem; border-radius: 6px; background: #e8eef3; }
.branch-true.active { background: #0a7d33; color: #fff; }
.branch-false.active { background: #c92f3c; color: #fff; }
.branch-sep { color: #aaa; font-weight: 400; }
/* Secret value display */
.secret-row { display: flex; align-items: center; gap: .7rem; margin-bottom: .8rem; }
.secret-box { font: 700 1.4rem ui-monospace, monospace; background: #1d3557; color: #fff;
padding: .25rem .6rem; border-radius: 6px; letter-spacing: .05em; min-width: 2.5rem;
text-align: center; }
/* Probe timing grid */
.probe-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; margin-bottom: .7rem; }
.probe-slot { height: 36px; border-radius: 6px; background: #d5dfe8; position: relative;
display: flex; align-items: flex-end; justify-content: center;
padding-bottom: 3px; font-size: .7rem; color: #555; transition: background .15s; }
.probe-slot.hot { background: #e63946; color: #fff; }
.probe-slot .slot-label { position: absolute; bottom: 4px; }
/* Status message */
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .6rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Spectre-style speculative execution demo — simplified visual simulation */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulation constants
const TRAIN_ROUNDS = 10; // training iterations to convince the predictor
const PROBE_SLOTS = 8; // slots in the probe array (values 0-7)
const SECRET = 5; // the secret byte value we want to leak (index 5)
// runtime state
let trainCount = 0;
let trainTimer = null;
let attackFired = false;
// DOM element references
const barTrain = document.getElementById('bar-train');
const countTrain = document.getElementById('count-train');
const bvisTrue = document.getElementById('bvis-true');
const bvisFalse = document.getElementById('bvis-false');
const btnTrain = document.getElementById('btn-train');
const panelTrain = document.getElementById('panel-train');
const panelAttack = document.getElementById('panel-attack');
const panelProbe = document.getElementById('panel-probe');
const secretDisp = document.getElementById('secret-display');
const btnAttack = document.getElementById('btn-attack');
const probeGrid = document.getElementById('probe-grid');
const statusProbe = document.getElementById('status-probe');
const btnReset = document.getElementById('btn-reset');
// Update the training progress bar
function updateTrainCount(n, total) {
const pct = (n / total) * 100;
barTrain.style.width = pct + '%';
countTrain.textContent = n + ' / ' + total;
}
// One training iteration: the branch is in-bounds, so the safe path is taken.
// The predictor increments its confidence counter for the taken (safe) direction.
function trainStep() {
trainCount++;
// simulated check: index < ARRAY_SIZE → true → speculative path taken
bvisTrue.classList.add('active');
bvisFalse.classList.remove('active');
updateTrainCount(trainCount, TRAIN_ROUNDS);
if (trainCount >= TRAIN_ROUNDS) {
clearInterval(trainTimer);
trainTimer = null;
// Training complete — transition to attack phase
setTimeout(() => {
panelTrain.style.display = 'none';
panelAttack.style.display = '';
}, 400);
}
}
btnTrain.addEventListener('click', () => {
// begin the training loop (180 ms per iteration)
if (trainTimer) return;
trainCount = 0;
bvisTrue.classList.remove('active');
bvisFalse.classList.remove('active');
updateTrainCount(0, TRAIN_ROUNDS);
trainTimer = setInterval(trainStep, 180);
});
// Attack phase: fire one out-of-bounds speculative read
btnAttack.addEventListener('click', () => {
if (attackFired) return;
attackFired = true;
// Step 1 — predictor predicts safe (taken), CPU speculatively reads array[SECRET]
// Predictor confident: index < ARRAY_SIZE predicts TRUE → speculatively reads array[SECRET]
bvisTrue.classList.add('active'); // predictor chose safe path
bvisFalse.classList.remove('active');
setTimeout(() => {
// Step 2 — CPU detects bounds violation, squashes architectural state
// Out-of-bounds detected → squash architectural state
bvisTrue.classList.remove('active');
bvisFalse.classList.add('active'); // out-of-bounds detected, architectural squash
setTimeout(() => {
// Step 3 — cache line stays warm despite architectural squash
// Cache line for probe_array[SECRET * stride] stays warm despite squash
secretDisp.textContent = '?';
panelAttack.style.display = 'none';
panelProbe.style.display = '';
buildProbeGrid();
}, 500);
}, 600);
});
// Build the probe grid showing which slot is hot
function buildProbeGrid() {
probeGrid.innerHTML = '';
// In a real attack timing probe_array[i * stride] — the hot slot is fast
for (let i = 0; i < PROBE_SLOTS; i++) {
const slot = document.createElement('div');
slot.className = 'probe-slot' + (i === SECRET ? ' hot' : '');
// label shows the slot index (0-7)
const lbl = document.createElement('span');
lbl.className = 'slot-label';
lbl.textContent = i;
slot.appendChild(lbl);
probeGrid.appendChild(slot);
}
// The hot slot index IS the secret value
statusProbe.textContent = i18nFmt('Cache timing reveals secret = %v% (slot %v% was fast).', { v: SECRET });
statusProbe.className = 'status ok';
secretDisp.textContent = SECRET;
}
btnReset.addEventListener('click', () => {
// Reset all phases to initial state
trainCount = 0;
attackFired = false;
if (trainTimer) { clearInterval(trainTimer); trainTimer = null; }
updateTrainCount(0, TRAIN_ROUNDS);
bvisTrue.classList.remove('active');
bvisFalse.classList.remove('active');
secretDisp.textContent = '?';
probeGrid.innerHTML = '';
statusProbe.textContent = '';
statusProbe.className = 'status';
panelTrain.style.display = '';
panelAttack.style.display = 'none';
panelProbe.style.display = 'none';
});
<!-- Three-phase Spectre-style demo: Train → Attack → Probe -->
<div class="panel" id="panel-train">
<h3>Phase 1 — Train the Branch Predictor</h3>
<p class="desc">We call victim_fn(i) for i = 0…9, always within bounds. The branch predictor learns: index < array.length → always true → take the safe path.</p>
<div class="progress-row">
<span class="label">Training iteration:</span>
<div class="bar-wrap"><div class="bar" id="bar-train"></div></div>
<span class="count" id="count-train">0 / 10</span>
</div>
<div class="branch-vis" id="branch-vis">
<span class="branch-true" id="bvis-true">✓ safe path</span>
<span class="branch-sep">|</span>
<span class="branch-false" id="bvis-false">✗ out of bounds</span>
</div>
<button id="btn-train" type="button">Start Training</button>
</div>
<div class="panel" id="panel-attack" style="display:none">
<h3>Phase 2 — Fire the Malicious Access</h3>
<p class="desc">Now we call victim_fn(secret_offset) where secret_offset points outside the array. The predictor — still confident — speculatively executes the safe path, reading array[secret_offset] and warming a cache line. The CPU then detects the bounds violation and squashes the result. The value never reaches a register, but the cache remembers.</p>
<div class="secret-row">
<span class="label">Secret value:</span>
<span class="secret-box" id="secret-display">?</span>
<span class="label small">(hidden from program; only the cache knows)</span>
</div>
<button id="btn-attack" type="button">Fire Malicious Access</button>
</div>
<div class="panel" id="panel-probe" style="display:none">
<h3>Phase 3 — Read the Cache (Timing Probe)</h3>
<p class="desc">We time access to each probe slot. The hot slot (fast access) reveals which cache line was touched during speculation — exposing the secret value.</p>
<div id="probe-grid" class="probe-grid"></div>
<div class="status" id="status-probe"></div>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Layout for the three-phase Spectre demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; }
.panel { background: #f4f7fa; border: 1px solid #d0dae4; border-radius: 10px;
padding: 1rem 1.1rem; margin-bottom: .8rem; }
h3 { font-size: 1rem; font-weight: 700; margin-bottom: .4rem; color: #1d3557; }
.desc { font-size: .88rem; color: #444; line-height: 1.5; margin-bottom: .8rem; }
/* Training progress bar */
.progress-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; }
.label { font-size: .82rem; color: #555; white-space: nowrap; }
.label.small { font-size: .78rem; color: #777; }
.bar-wrap { flex: 1; height: 10px; background: #d5dfe8; border-radius: 5px; overflow: hidden; }
.bar { height: 100%; width: 0; background: #457b9d; border-radius: 5px; transition: width .12s; }
.count { font-size: .82rem; color: #555; white-space: nowrap; min-width: 3rem; text-align: right; }
/* Branch predictor visualizer */
.branch-vis { display: flex; align-items: center; gap: .5rem; margin-bottom: .8rem;
font-size: .85rem; font-weight: 600; }
.branch-true { color: #0a7d33; padding: .25rem .55rem; border-radius: 6px; background: #d4f0dc; }
.branch-false { color: #888; padding: .25rem .55rem; border-radius: 6px; background: #e8eef3; }
.branch-true.active { background: #0a7d33; color: #fff; }
.branch-false.active { background: #c92f3c; color: #fff; }
.branch-sep { color: #aaa; font-weight: 400; }
/* Secret value display */
.secret-row { display: flex; align-items: center; gap: .7rem; margin-bottom: .8rem; }
.secret-box { font: 700 1.4rem ui-monospace, monospace; background: #1d3557; color: #fff;
padding: .25rem .6rem; border-radius: 6px; letter-spacing: .05em; min-width: 2.5rem;
text-align: center; }
/* Probe timing grid */
.probe-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; margin-bottom: .7rem; }
.probe-slot { height: 36px; border-radius: 6px; background: #d5dfe8; position: relative;
display: flex; align-items: flex-end; justify-content: center;
padding-bottom: 3px; font-size: .7rem; color: #555; transition: background .15s; }
.probe-slot.hot { background: #e63946; color: #fff; }
.probe-slot .slot-label { position: absolute; bottom: 4px; }
/* Status message */
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .6rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Spectre-style speculative execution demo — simplified visual simulation */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulation constants
const TRAIN_ROUNDS = 10; // training iterations to convince the predictor
const PROBE_SLOTS = 8; // slots in the probe array (values 0-7)
const SECRET = 5; // the secret byte value we want to leak (index 5)
// runtime state
let trainCount = 0;
let trainTimer = null;
let attackFired = false;
// DOM element references
const barTrain = document.getElementById('bar-train');
const countTrain = document.getElementById('count-train');
const bvisTrue = document.getElementById('bvis-true');
const bvisFalse = document.getElementById('bvis-false');
const btnTrain = document.getElementById('btn-train');
const panelTrain = document.getElementById('panel-train');
const panelAttack = document.getElementById('panel-attack');
const panelProbe = document.getElementById('panel-probe');
const secretDisp = document.getElementById('secret-display');
const btnAttack = document.getElementById('btn-attack');
const probeGrid = document.getElementById('probe-grid');
const statusProbe = document.getElementById('status-probe');
const btnReset = document.getElementById('btn-reset');
// Update the training progress bar
function updateTrainCount(n, total) {
const pct = (n / total) * 100;
barTrain.style.width = pct + '%';
countTrain.textContent = n + ' / ' + total;
}
// One training iteration: the branch is in-bounds, so the safe path is taken.
// The predictor increments its confidence counter for the taken (safe) direction.
function trainStep() {
trainCount++;
// simulated check: index < ARRAY_SIZE → true → speculative path taken
bvisTrue.classList.add('active');
bvisFalse.classList.remove('active');
updateTrainCount(trainCount, TRAIN_ROUNDS);
if (trainCount >= TRAIN_ROUNDS) {
clearInterval(trainTimer);
trainTimer = null;
// Training complete — transition to attack phase
setTimeout(() => {
panelTrain.style.display = 'none';
panelAttack.style.display = '';
}, 400);
}
}
btnTrain.addEventListener('click', () => {
// begin the training loop (180 ms per iteration)
if (trainTimer) return;
trainCount = 0;
bvisTrue.classList.remove('active');
bvisFalse.classList.remove('active');
updateTrainCount(0, TRAIN_ROUNDS);
trainTimer = setInterval(trainStep, 180);
});
// Attack phase: fire one out-of-bounds speculative read
btnAttack.addEventListener('click', () => {
if (attackFired) return;
attackFired = true;
// Step 1 — predictor predicts safe (taken), CPU speculatively reads array[SECRET]
// Predictor confident: index < ARRAY_SIZE predicts TRUE → speculatively reads array[SECRET]
bvisTrue.classList.add('active'); // predictor chose safe path
bvisFalse.classList.remove('active');
setTimeout(() => {
// Step 2 — CPU detects bounds violation, squashes architectural state
// Out-of-bounds detected → squash architectural state
bvisTrue.classList.remove('active');
bvisFalse.classList.add('active'); // out-of-bounds detected, architectural squash
setTimeout(() => {
// Step 3 — cache line stays warm despite architectural squash
// Cache line for probe_array[SECRET * stride] stays warm despite squash
secretDisp.textContent = '?';
panelAttack.style.display = 'none';
panelProbe.style.display = '';
buildProbeGrid();
}, 500);
}, 600);
});
// Build the probe grid showing which slot is hot
function buildProbeGrid() {
probeGrid.innerHTML = '';
// In a real attack timing probe_array[i * stride] — the hot slot is fast
for (let i = 0; i < PROBE_SLOTS; i++) {
const slot = document.createElement('div');
slot.className = 'probe-slot' + (i === SECRET ? ' hot' : '');
// label shows the slot index (0-7)
const lbl = document.createElement('span');
lbl.className = 'slot-label';
lbl.textContent = i;
slot.appendChild(lbl);
probeGrid.appendChild(slot);
}
// The hot slot index IS the secret value
statusProbe.textContent = i18nFmt('Cache timing reveals secret = %v% (slot %v% was fast).', { v: SECRET });
statusProbe.className = 'status ok';
secretDisp.textContent = SECRET;
}
btnReset.addEventListener('click', () => {
// Reset all phases to initial state
trainCount = 0;
attackFired = false;
if (trainTimer) { clearInterval(trainTimer); trainTimer = null; }
updateTrainCount(0, TRAIN_ROUNDS);
bvisTrue.classList.remove('active');
bvisFalse.classList.remove('active');
secretDisp.textContent = '?';
probeGrid.innerHTML = '';
statusProbe.textContent = '';
statusProbe.className = 'status';
panelTrain.style.display = '';
panelAttack.style.display = 'none';
panelProbe.style.display = 'none';
});
Notice how the secret leaks through timing alone. The architectural check (index < array.length) was satisfied every time during training, so the predictor is confident. On the malicious access it predicts "safe," speculatively reads array[secret_offset], and populates a cache line — before the CPU notices the index is out of bounds and squashes the result. The squash removes the value from registers but the cache line stays warm. Measuring which probe slot is fast reveals the secret.
Comments
Loading comments...