# getChannelData fingerprint noise that compounded

> getChannelData returns the buffer's own memory, so our audio noise multiplied samples on every read: v·f, then v·f². A seeded byte fixed it.

Source: https://mosaicbrowser.com/blog/getchanneldata-compounding

## An audio spoof that changed the sound every time it was read

getChannelData returns the buffer's own memory, so our audio noise multiplied samples on every read: v·f, then v·f². A seeded byte fixed it.

![Read it twice: v·f, then v·f·f, beside a screenshot: a Mosaic leak test: every surface a site reads, checked from inside the profile page](https://mosaicbrowser.com/covers/getchanneldata-compounding.webp)

`AudioBuffer.getChannelData()` does not hand a page a copy of the samples. It hands back a `Float32Array` over the buffer's own memory. Our audio fingerprint noise multiplied every 149th sample of that array in place. So every read changed the real audio again, and the noise compounded: a sample came back as `v·f` on the first read and `v·f²` on the second. A page that reads the same buffer twice and compares the two caught it at once.

The bug was in a JavaScript fallback written for the days when Mosaic could run a profile on a stock Chromium. Mosaic now launches profiles only on Tesseract Chrome, our own engine, where audio values are changed in C++ and no script touches them. But the fallback was ours too, and the way it broke applies to any noise you put on a surface a page can read more than once.

## How a getChannelData fingerprint is taken

An audio fingerprint needs no microphone and makes no sound. The page builds an `OfflineAudioContext`, runs an oscillator through a compressor, renders the result into an `AudioBuffer`, and reads the samples with `getChannelData(0)`. Then it sums or hashes them. The floating-point maths of the audio stack differs slightly between machines and builds, so the number differs too, and it stays put on one machine.

Noise is meant to move that number per profile, so two profiles on one computer do not share it. The question is how you move it.

## What multiplying in place did

The first version of our noise took the array `getChannelData` returned and multiplied every 149th sample by a small drift factor. It looked harmless, because the change was far below anything you could hear.

But the array is the buffer. The multiplication went into the page's own audio and stayed there. Read the buffer again, and the noise was applied again, to samples that already carried it:

```
read 1   v · f
read 2   v · f · f
read 3   v · f · f · f

```

Pixelscan's audio check reads the same surface twice and compares the results, and this was the one surface in the script that failed it. Measured on four fresh profiles on one machine, with nothing changed between them except the seed, the check came back consistent, inconsistent, consistent, inconsistent. A real device never reads two different values from one buffer. So the noise was worse than useless: it produced a reading no real browser produces.

Canvas noise had already been through this fix: stamp the pixels, never scale them. Audio was the one surface that rule had never reached.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## The fix: set, don't scale

The rule from canvas carried over word for word: perturb by assignment, keyed on the sample's position rather than on the call. For each sample we stamp, the script now sets the float's lowest mantissa byte to a value drawn from the profile's seed. The floats are little-endian, so that byte is the first of each four, and a `Uint8Array` over the same memory reaches it without disturbing anything else.

Assignment is what makes it idempotent. The second read finds the byte already at its seeded value and writes the same value again. Measured after the change:

```
read 1            495.401644082944
read 2            the same
read 3            the same
copyFromChannel   the same

```

The first version of the stamp set a single bit, not a byte. That was too small. Across six profiles the audio fingerprint came out only 3 distinct values, because a one-bit change on a sample sits below the resolution of a hash taken over a rounded sum, so profiles collided. A whole low byte moves the sample by about 1e-5 of its value. That is about what the old multiplication moved it by, and still far below hearing.

## Two routes to one surface

`getChannelData` is not the only way to read samples. `copyFromChannel` copies them into an array the page owns, and before the fix it returned the raw buffer while `getChannelData` returned a stamped one. A page calling both would have seen two different sounds from one buffer, the same contradiction as reading twice.

So `copyFromChannel` now applies the same stamp to the copy, at the same positions, offset by where the copy starts. Because the stamp is idempotent, it cannot disagree with the original however the two calls are interleaved. Canvas taught us this half too: our noise once reached `toDataURL` and missed `toBlob`, which is in [the canvas method nobody noised](https://mosaicbrowser.com/blog/the-canvas-method-nobody-noised).

## Why it still writes into the buffer

The obvious alternative is to return a stamped copy and leave the buffer alone. We didn't, on purpose. `getChannelData` is also how a page writes audio: a synthesiser fills the array it gets back and plays the buffer. Return a copy and everything the page wrote is silently thrown away. Changing one low byte of every 149th sample, which nobody can hear, costs less than breaking every page that makes sound.

## The question that finds this class of bug

The useful test was never "does the noise change the fingerprint". It was "does it change it **twice**". The same test found our geolocation answering the first call and timing out on every one after it, which is in [geolocation that answered once](https://mosaicbrowser.com/blog/geolocation-answered-once). A first read is what every manual test does, and the second read is where the bug was.

It is also part of why Mosaic's engine moves values in C++ rather than in page script. There the browser computes the value itself and every route out of a surface reads the same thing. The case for that, and what a script cannot reach, is in [why we change fingerprints in C++](https://mosaicbrowser.com/blog/cpp-not-javascript). How a page builds a fingerprint from canvas reads, the cousin of this one, is in [canvas fingerprinting](https://mosaicbrowser.com/glossary/canvas-fingerprinting).

## Questions

### What is a getChannelData fingerprint?

A page renders a short sound in an OfflineAudioContext, reads the samples back with getChannelData and hashes or sums them. Small differences in how a machine's audio stack computes the samples give each setup its own number.

### Does getChannelData return a copy of the audio?

No. It returns a Float32Array over the AudioBuffer's own storage. Writing into that array changes the buffer, which is also how a page writes audio it has generated itself.

### How do you add audio fingerprint noise without breaking a read-twice check?

Set chosen sample bits to a seeded value by assignment rather than multiplying them. The second read finds the bits already set and changes nothing, and copyFromChannel has to carry the same stamp so both routes agree.

### Does Mosaic's engine add audio noise with JavaScript?

No. On Tesseract Chrome the audio values move in C++, inside the engine. The bug here was in a script fallback for stock Chromium, which Mosaic no longer launches profiles on.

## Related

[The canvas method nobody noised: what a worker test found](https://mosaicbrowser.com/blog/the-canvas-method-nobody-noised): Our canvas noise reached toDataURL and missed toBlob, so a page calling toBlob got a PNG byte-identical to stock Chrome's. A worker test caught it.

[Why we change fingerprints in C++, not JavaScript](https://mosaicbrowser.com/blog/cpp-not-javascript): A JavaScript spoof is one realm deep. An iframe read the real source of 33 of our patched functions; the same values changed in C++ agree everywhere.

[A spoof that answers once: the geolocation bug we shipped](https://mosaicbrowser.com/blog/geolocation-answered-once): A geolocation spoofing timeout we shipped: the first getCurrentPosition got the exit's coordinates, every later call failed with code 3. One latch was to blame.

[Canvas fingerprinting: how a drawing identifies your computer](https://mosaicbrowser.com/glossary/canvas-fingerprinting): Canvas fingerprinting draws hidden text and shapes, reads the pixels back and hashes them. Small rendering differences make the hash a mark of your machine.









## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
