Introduction

Modern computers store every bit as a tiny electric charge in a DRAM cell — a capacitor paired with a transistor, etched onto silicon at nanometer scale. Billions of them sit in your RAM module, packed into rows on a chip.

The problem is leakage. Every time a row of cells is activated (read or refreshed), electromagnetic coupling injects a small charge disturbance into neighboring rows. Under normal use the disturbance is negligible — the memory controller refreshes cells thousands of times per second to compensate. But if you repeatedly hammer the same row — activating it hundreds of thousands of times per second — the accumulated charge disturbance can flip a bit in an adjacent row from 1 to 0 or vice versa.

This is Rowhammer, documented thoroughly by Kim et al. in their 2014 ISCA paper. The crucial twist: you never need to write to the victim row. Reading your own memory, over and over, can corrupt memory that belongs to the operating system or another process entirely — breaking the memory-isolation guarantee that modern security depends on.

Hammer the Row

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();
Result
Console

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).

The Real Complexity

Rowhammer is surprising not just as a physics curiosity but as a security break:

  • Memory isolation is the bedrock assumption of every modern OS: process A cannot modify process B's memory, nor the kernel's. Virtual memory and hardware page tables enforce this in software and hardware.
  • Rowhammer bypasses all of it. An unprivileged process can hammer a DRAM row it legally owns. The resulting bit-flip lands in a physically adjacent row — which may belong to the kernel or another process. The CPU page tables see nothing wrong; the corruption happens below the abstraction layer they control.
  • The flip rate depends on DRAM technology. As cells shrink with each process node (<30 nm< 30\,\text{nm}), the capacitors are closer together and leakage worsens. Newer LPDDR4 and DDR5 modules can flip bits with as few as ∼10,000\sim 10{,}000 hammer activations — much less than the typical refresh interval of ∼64,000\sim 64{,}000 activations per 64 ms64\,\text{ms}.
  • Double-sided Rowhammer hammers two rows on either side of a victim, doubling the electromagnetic stress and lowering the threshold further.
  • Exploits are real. Google Project Zero (2015) demonstrated a privilege escalation to kernel level using only JavaScript and unprivileged x86 instructions. Subsequent work extended the attack to sandboxed browsers, virtual machines, and even network packets (RAMBleed, NetHammer).

Mitigations include Target Row Refresh (TRR) in DDR4/5, ECC (which catches single-bit flips), LPDDR5 PRAC (per-row activation counting), and OS-level strategies that avoid placing sensitive pages in physically adjacent rows. But mitigations are a cat-and-mouse game: researchers keep finding ways around each generation of defenses.

Where It Matters

Rowhammer is not a theoretical curiosity — it has shaped real security decisions and DRAM standards:

  • Browser sandboxes: JavaScript in a tab can trigger Rowhammer against the browser process or OS, even without native code execution. Browsers now use memory randomization and site isolation partly in response.
  • Cloud multi-tenancy: virtual machines on the same physical host share DRAM modules. A malicious VM can flip bits in a co-resident VM's pages, breaking hypervisor isolation. Cloud providers responded with memory scrambling and ECC mandates.
  • DRAM standards: DDR4 introduced TRR (Target Row Refresh) and pTRR — the memory controller monitors hot rows and issues extra refreshes. DDR5 mandates PRAC (Per-Row Activation Counting) with a hardware counter per row. ECC catches single-bit errors but adds cost and complexity.
  • Cryptographic keys: RAMBleed (2019) showed that Rowhammer can be used not just to flip bits but to read secret memory indirectly — leaking RSA private keys from an adjacent process.
  • Embedded and IoT: many low-cost devices use non-ECC LPDDR memory with no TRR, remaining permanently vulnerable.

Rowhammer sits at the intersection of hardware physics, memory safety, and systems security — a reminder that even correct software on a correct OS can be subverted by the physics of the hardware beneath it.

Conclusion

Rowhammer breaks a tacit promise at the heart of computing: that a program which only reads its own memory cannot harm anyone else's. By exploiting the physics of electron leakage in densely packed DRAM cells, an attacker can flip bits in memory they are never supposed to touch — bypassing the OS, the hypervisor, and every software-level isolation boundary at once.

Each generation of DRAM brings smaller cells, tighter packing, and a lower flip threshold. Hardware defenses like TRR and PRAC raise the bar, but researchers continue finding ways around them. Rowhammer is ultimately a lesson that memory safety is not just a software problem — it reaches all the way down to the physics of the silicon.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/rowhammer/Content licensed under CC BY-NC 4.0.