The simulation below models a small DRAM bank. The aggressor row (red) is hammered repeatedly. Each bar is a bit in the neighboring victim row — watch for flips as the hammer count climbs.
<!-- Rowhammer demo: simulates DRAM bit-flips caused by repeatedly activating an aggressor row. -->
<div class="controls">
<label>Hammer count: <span id="hammerVal">50000</span>
<input type="range" id="hammerSlider" min="10000" max="200000" step="10000" value="50000">
</label>
<label>Row distance:
<select id="rowDist">
<option value="1">1 (adjacent)</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</label>
<label class="ecc-label">
<input type="checkbox" id="eccToggle"> ECC
</label>
</div>
<div class="scene">
<div class="row-label aggressor-label">Aggressor Row (hammered)</div>
<div id="aggressorRow" class="dram-row aggressor"></div>
<div class="row-gap" id="gapLabel">2 rows apart</div>
<div class="row-label victim-label">Victim Row (neighbor)</div>
<div id="victimRow" class="dram-row victim"></div>
<div id="flipCount" class="flip-count"></div>
</div>
<div class="btns">
<button id="hammerBtn" type="button">Hammer!</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
/* Rowhammer demo styles: aggressor row in red, victim bits in blue, flipped bits in red, ECC-corrected in green. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem 1.2rem; margin-bottom: .8rem; align-items: center; }
.controls label { display: flex; align-items: center; gap: .4rem; font-size: .88rem; }
input[type=range] { width: 120px; }
select { font-size: .88rem; padding: .15rem .3rem; border-radius: 6px; border: 1px solid #adb1b8; }
.ecc-label { font-weight: 600; cursor: pointer; }
.scene { margin: .5rem 0 .6rem; }
.row-label { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .2rem; }
.aggressor-label { color: #c92f3c; }
.victim-label { color: #1d3557; }
.row-gap { font-size: .75rem; color: #666; margin: .35rem 0; font-style: italic; }
.dram-row { display: flex; gap: 3px; flex-wrap: wrap; }
.cell { width: 20px; height: 32px; border-radius: 4px; transition: background .2s; }
.aggressor .cell { background: #e63946; }
.aggressor .cell.active { background: #ff6b6b; }
.victim .cell { background: #457b9d; }
.victim .cell.flipped { background: #e63946; }
.victim .cell.ecc-fixed { background: #0a7d33; }
.flip-count { font-size: .85rem; margin: .4rem 0; color: #444; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.flip { color: #c92f3c; }
.status.ecc { color: #e07a00; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified DRAM Rowhammer model.
// Real Rowhammer depends on DRAM chip, process node, temperature, and memory controller.
const BITS = 32; // number of bits displayed per row
// Probability that a given victim bit flips after hammerCount activations at rowDistance.
// Modeled as: base * distance_decay^(dist-1) * min(1, count/threshold).
function flipProbability(hammerCount, rowDistance) {
const base = 0.6;
const distanceFactor = Math.pow(0.45, rowDistance - 1);
const countFactor = Math.min(1, hammerCount / 150000);
return base * distanceFactor * countFactor;
}
// Demo state: which victim bits are flipped and which were corrected by ECC.
let victimBits = new Array(BITS).fill(0);
let flippedIndices = new Set();
let eccFixed = new Set();
let running = false;
function getParams() {
return {
hammerCount: parseInt(document.getElementById('hammerSlider').value),
rowDist: parseInt(document.getElementById('rowDist').value),
ecc: document.getElementById('eccToggle').checked
};
}
function renderAggressor() {
// Draw the aggressor row cells (always unflipped, just flashing during hammer).
const row = document.getElementById('aggressorRow');
row.innerHTML = '';
for (let i = 0; i < BITS; i++) {
const cell = document.createElement('div');
cell.className = 'cell';
row.appendChild(cell);
}
}
function renderVictim() {
// Draw victim row cells, coloring flipped and ECC-corrected bits differently.
const row = document.getElementById('victimRow');
row.innerHTML = '';
for (let i = 0; i < BITS; i++) {
const cell = document.createElement('div');
cell.className = 'cell' + (flippedIndices.has(i) ? (eccFixed.has(i) ? ' ecc-fixed' : ' flipped') : '');
row.appendChild(cell);
}
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + cls;
}
function updateFlipCount() {
const raw = flippedIndices.size;
const corrected = eccFixed.size;
const visible = raw - corrected;
const el = document.getElementById('flipCount');
if (raw === 0) {
el.textContent = '';
} else {
el.textContent = i18nFmt('%raw% bit(s) flipped · %corrected% corrected by ECC · %visible% visible.', { raw: raw, corrected: corrected, visible: visible });
}
}
function hammer() {
if (running) return;
running = true;
const { hammerCount, rowDist, ecc } = getParams();
const p = flipProbability(hammerCount, rowDist);
// Animate the aggressor row flashing to visualize the hammering.
const aggressorCells = document.querySelectorAll('.aggressor .cell');
let frame = 0;
const flashInterval = setInterval(() => {
aggressorCells.forEach((c, i) => {
c.classList.toggle('active', (i + frame) % 3 === 0);
});
frame++;
if (frame > 8) clearInterval(flashInterval);
}, 80);
setTimeout(() => {
// Determine which victim bits flip based on probability model.
flippedIndices = new Set();
eccFixed = new Set();
for (let i = 0; i < BITS; i++) {
if (Math.random() < p) {
flippedIndices.add(i);
}
}
// Apply ECC: correct single-bit errors per word; double-bit errors are uncorrectable.
// ECC can correct 1 bit error per 64-bit word (8 bits here for display)
const WORD = 8;
for (let w = 0; w < BITS / WORD; w++) {
const wordFlips = [];
for (let b = w * WORD; b < (w + 1) * WORD; b++) {
if (flippedIndices.has(b)) wordFlips.push(b);
}
if (ecc && wordFlips.length === 1) {
eccFixed.add(wordFlips[0]);
}
}
aggressorCells.forEach(c => c.classList.remove('active'));
renderVictim();
updateFlipCount();
const visibleFlips = flippedIndices.size - eccFixed.size;
if (flippedIndices.size === 0) {
setStatus(i18nFmt('No bit-flips at this distance and count (flip probability: %p%%).', { p: (p * 100).toFixed(1) }), 'ok');
} else if (visibleFlips === 0) {
setStatus(i18nFmt('ECC corrected all %raw% flip(s) — memory intact!', { raw: flippedIndices.size }), 'ecc');
} else {
setStatus(i18nFmt('%n% bit(s) flipped in the victim row — memory corrupted!', { n: visibleFlips }), 'flip');
}
running = false;
}, 800);
}
function reset() {
flippedIndices = new Set();
eccFixed = new Set();
renderAggressor();
renderVictim();
updateFlipCount();
setStatus('', '');
}
document.getElementById('hammerSlider').addEventListener('input', () => {
document.getElementById('hammerVal').textContent = parseInt(document.getElementById('hammerSlider').value).toLocaleString();
});
document.getElementById('rowDist').addEventListener('change', () => {
const d = parseInt(document.getElementById('rowDist').value);
document.getElementById('gapLabel').textContent = i18nFmt('%d% rows apart', { d: d });
});
document.getElementById('hammerBtn').addEventListener('click', hammer);
document.getElementById('resetBtn').addEventListener('click', reset);
renderAggressor();
renderVictim();
<!-- Rowhammer demo: simulates DRAM bit-flips caused by repeatedly activating an aggressor row. -->
<div class="controls">
<label>Hammer count: <span id="hammerVal">50000</span>
<input type="range" id="hammerSlider" min="10000" max="200000" step="10000" value="50000">
</label>
<label>Row distance:
<select id="rowDist">
<option value="1">1 (adjacent)</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</label>
<label class="ecc-label">
<input type="checkbox" id="eccToggle"> ECC
</label>
</div>
<div class="scene">
<div class="row-label aggressor-label">Aggressor Row (hammered)</div>
<div id="aggressorRow" class="dram-row aggressor"></div>
<div class="row-gap" id="gapLabel">2 rows apart</div>
<div class="row-label victim-label">Victim Row (neighbor)</div>
<div id="victimRow" class="dram-row victim"></div>
<div id="flipCount" class="flip-count"></div>
</div>
<div class="btns">
<button id="hammerBtn" type="button">Hammer!</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
/* Rowhammer demo styles: aggressor row in red, victim bits in blue, flipped bits in red, ECC-corrected in green. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem 1.2rem; margin-bottom: .8rem; align-items: center; }
.controls label { display: flex; align-items: center; gap: .4rem; font-size: .88rem; }
input[type=range] { width: 120px; }
select { font-size: .88rem; padding: .15rem .3rem; border-radius: 6px; border: 1px solid #adb1b8; }
.ecc-label { font-weight: 600; cursor: pointer; }
.scene { margin: .5rem 0 .6rem; }
.row-label { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .2rem; }
.aggressor-label { color: #c92f3c; }
.victim-label { color: #1d3557; }
.row-gap { font-size: .75rem; color: #666; margin: .35rem 0; font-style: italic; }
.dram-row { display: flex; gap: 3px; flex-wrap: wrap; }
.cell { width: 20px; height: 32px; border-radius: 4px; transition: background .2s; }
.aggressor .cell { background: #e63946; }
.aggressor .cell.active { background: #ff6b6b; }
.victim .cell { background: #457b9d; }
.victim .cell.flipped { background: #e63946; }
.victim .cell.ecc-fixed { background: #0a7d33; }
.flip-count { font-size: .85rem; margin: .4rem 0; color: #444; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.flip { color: #c92f3c; }
.status.ecc { color: #e07a00; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified DRAM Rowhammer model.
// Real Rowhammer depends on DRAM chip, process node, temperature, and memory controller.
const BITS = 32; // number of bits displayed per row
// Probability that a given victim bit flips after hammerCount activations at rowDistance.
// Modeled as: base * distance_decay^(dist-1) * min(1, count/threshold).
function flipProbability(hammerCount, rowDistance) {
const base = 0.6;
const distanceFactor = Math.pow(0.45, rowDistance - 1);
const countFactor = Math.min(1, hammerCount / 150000);
return base * distanceFactor * countFactor;
}
// Demo state: which victim bits are flipped and which were corrected by ECC.
let victimBits = new Array(BITS).fill(0);
let flippedIndices = new Set();
let eccFixed = new Set();
let running = false;
function getParams() {
return {
hammerCount: parseInt(document.getElementById('hammerSlider').value),
rowDist: parseInt(document.getElementById('rowDist').value),
ecc: document.getElementById('eccToggle').checked
};
}
function renderAggressor() {
// Draw the aggressor row cells (always unflipped, just flashing during hammer).
const row = document.getElementById('aggressorRow');
row.innerHTML = '';
for (let i = 0; i < BITS; i++) {
const cell = document.createElement('div');
cell.className = 'cell';
row.appendChild(cell);
}
}
function renderVictim() {
// Draw victim row cells, coloring flipped and ECC-corrected bits differently.
const row = document.getElementById('victimRow');
row.innerHTML = '';
for (let i = 0; i < BITS; i++) {
const cell = document.createElement('div');
cell.className = 'cell' + (flippedIndices.has(i) ? (eccFixed.has(i) ? ' ecc-fixed' : ' flipped') : '');
row.appendChild(cell);
}
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + cls;
}
function updateFlipCount() {
const raw = flippedIndices.size;
const corrected = eccFixed.size;
const visible = raw - corrected;
const el = document.getElementById('flipCount');
if (raw === 0) {
el.textContent = '';
} else {
el.textContent = i18nFmt('%raw% bit(s) flipped · %corrected% corrected by ECC · %visible% visible.', { raw: raw, corrected: corrected, visible: visible });
}
}
function hammer() {
if (running) return;
running = true;
const { hammerCount, rowDist, ecc } = getParams();
const p = flipProbability(hammerCount, rowDist);
// Animate the aggressor row flashing to visualize the hammering.
const aggressorCells = document.querySelectorAll('.aggressor .cell');
let frame = 0;
const flashInterval = setInterval(() => {
aggressorCells.forEach((c, i) => {
c.classList.toggle('active', (i + frame) % 3 === 0);
});
frame++;
if (frame > 8) clearInterval(flashInterval);
}, 80);
setTimeout(() => {
// Determine which victim bits flip based on probability model.
flippedIndices = new Set();
eccFixed = new Set();
for (let i = 0; i < BITS; i++) {
if (Math.random() < p) {
flippedIndices.add(i);
}
}
// Apply ECC: correct single-bit errors per word; double-bit errors are uncorrectable.
// ECC can correct 1 bit error per 64-bit word (8 bits here for display)
const WORD = 8;
for (let w = 0; w < BITS / WORD; w++) {
const wordFlips = [];
for (let b = w * WORD; b < (w + 1) * WORD; b++) {
if (flippedIndices.has(b)) wordFlips.push(b);
}
if (ecc && wordFlips.length === 1) {
eccFixed.add(wordFlips[0]);
}
}
aggressorCells.forEach(c => c.classList.remove('active'));
renderVictim();
updateFlipCount();
const visibleFlips = flippedIndices.size - eccFixed.size;
if (flippedIndices.size === 0) {
setStatus(i18nFmt('No bit-flips at this distance and count (flip probability: %p%%).', { p: (p * 100).toFixed(1) }), 'ok');
} else if (visibleFlips === 0) {
setStatus(i18nFmt('ECC corrected all %raw% flip(s) — memory intact!', { raw: flippedIndices.size }), 'ecc');
} else {
setStatus(i18nFmt('%n% bit(s) flipped in the victim row — memory corrupted!', { n: visibleFlips }), 'flip');
}
running = false;
}, 800);
}
function reset() {
flippedIndices = new Set();
eccFixed = new Set();
renderAggressor();
renderVictim();
updateFlipCount();
setStatus('', '');
}
document.getElementById('hammerSlider').addEventListener('input', () => {
document.getElementById('hammerVal').textContent = parseInt(document.getElementById('hammerSlider').value).toLocaleString();
});
document.getElementById('rowDist').addEventListener('change', () => {
const d = parseInt(document.getElementById('rowDist').value);
document.getElementById('gapLabel').textContent = i18nFmt('%d% rows apart', { d: d });
});
document.getElementById('hammerBtn').addEventListener('click', hammer);
document.getElementById('resetBtn').addEventListener('click', reset);
renderAggressor();
renderVictim();
Notice that flips become more likely as you increase the hammer count and reduce the row distance. Toggle ECC to see error-correcting code absorb single-bit flips — but note that ECC cannot correct two simultaneous flips in the same word, which some attacks deliberately induce (double-sided Rowhammer).
Comments
Loading comments...