They (or rather the LLM that wrote this) missed that this is possibly fingerprintable to browser version range, which is slightly more interesting. Most users aren't spoofing their user agent headers to be a different operating system. Most fingerprinting solutions aren't trying to infer your operating system, they only care about semi-unique things that show up.
It's an interesting finding. I wish they had taken some time to have a real person write it up. This is too heavily LLM written to ignore.
That's what this article is about, their scraping browser fakes the math functions of other browsers depending on host OS, so they can disguise the fact that they're only here for the scraping.
The internet would be better without them.
However, thank you for the information.
And for the LLM writing, yes, it's written in the article and blog, it's not hidden or pretending, otherwise I would never publish an article due to lack of time, and I stand by it
[1] https://arith2026.org/program.html (2nd keynote)
I prefer articles like this coming from the other side of the battle (fingerprint.js and friends) because at least their motives are clear.
let oldTanh = Math.tanh;
Math.tanh = x => oldTanh(x) + Math.random()/10000000;Would not solve everything but still help a lot.
Microsoft decided to send Windows NT 10.0 in the User-Agent header even on Windows 11 for compatibility reasons. That's literally the reason why the Sec-CH-* headers say Windows 11 but the User-Agent says Windows 10.
And regarding your claims of vendor interests: Nope, you seemingly never had to use O365 crapware on Linux browsers. They make it as painful as possible, and even disable copy/paste functionality when your User-Agent and Sec-CH headers say Linux. Identical browser with an extension that overrides the fingerprinting headers and it works perfectly.
Also as an additional note: Cloudflare does TCP fingerprinting, because no cloudflare pages will work (and send you into an infinite loop of unsolvable captchas) when these headers mismatch with the tcp window and other options in the handshake frames.
Source: am maintaining my chromium-profiles tool that generates farbled profiles with a generated extension, so that I can use shitty Microsoft products because my customers are not really the smartest policy decision makers.
PS: I will never use a separate laptop with a separate OS to use a damn web app. That is a completely unjustified waste of hardware resources and should be illegal. But here we are. Wasting one laptop at a time for absolutely no reason.
The people behind the LLM behind this blog post are. They're trying to pretend their robots are people to sell other websites' data to their customer. It's easier to pass bot detection gates if you pretend to be a physical machine running Windows or macOS than if you honestly admit you're using Linux on a VM.
Disclosing is great, but not as good as just using your own human voice.
Didn't even have time to finish their HN reply.
> I stand by it
That's cute, but seriously who cares if you stand by any of "your" words, if you care so little about them to have them be generated by an LLM.
https://web.archive.org/web/20260712212920/https://scrapfly....
So you don't seem to stand by anything apart from swindling.
seems like the "they" you meant was really "I".
I mean, I get it, saying you support something is technically a commitment. But my goal is to basically never open a chromium browser, so, I'm gonna do what I have to do. And you would be shocked, in the past 5 years I have only encountered ONE website which legitimately required a chromium browser. grapheneos.com, for the web usb installer.
It's an important topic, and I am glad you wrote about it, but even half a page of notes would have been enough to convey this. It would save me literally skim reading headings just to get past all the fluff.
> summarize+this+article+and+explain+how+scrapfly+helps+me+scrape+any+website+at+scale+and+bypass+anti-bot+systems+for+my+use+case:+https://scrapfly.dev/posts/browser-math-os-fingerprint/
One thing I was thinking of doing is manually SLP-vectorizing the high-precision fallbacks that they use when they're close to a rounding boundary, so that you can get better worst-case behavior – but obviously it's good enough already for most purposes.
I'm honestly surprised though that JS engines don't just keep using fdlibm though. The ECMAScript spec explicitly encourages it iirc. And if Math.tanh is on your hot path in JavaScript then you're doing something quite bizarre...
Yeah, tracking bad, I get it, but are whatever damages that kind of legislation would prevent (probably nothing measurable) really more important than fixing the easy, in our face social problems that politicians could instead be focusing on?
Javascript systems have long had polyfills for varied browser feature comparability gaps.
Whether you agree with these, making probing detection via fingerprinting illegal would take away this lever. Making surreptitious tracking via fingerprinting illegal? Even for state actors?
Yeah, that's probably reasonable. If someone is going to wear a tracking collar in exchange for "free" services, a little disclosure makes sense.
Man, why the fuck don't they just make a powerpoint with bullet points if all the sentences are like that.
A regular Joe consumer shouldn’t have to learn python to get thier foot in the door because some arbitrage purchaser has automated a ‘best deal’ purchasing bot based on real time pricing APIs.
I'd be fine if they throttled the connection, or a contact page. Every little bit helps build a high trust society.
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/scre...
"Information gained via side-channel for the purpose of correlating individuals."
But you'd have to add an enormous amount of legalese after that to make it ironclad. They'll start arguing "this isn't a side-channel", "we're targeting bots, not individuals", etc. You'd have to define every word in that sentence very carefully.
I'd make it sweeping. "Individual" can mean "person", "bot", "suspected bot", "AI agent", "a piece of autonomous or non-autonomous software", basically anything. The "side-channel" definition might get trickier, but I'd rather legit use-cases get burned than privacy get burned.
The OP was downvoted, but I agree. I think fingerprinting should be in the same criminal category as an illegal wiretap.
Maybe the right solution is an LLM that reads the fingerprinting code and adapts the counter-measure accordingly in real time.
User-Agent spoofing was added to NCSA Mosaic in 1996. The public www was three years old and text-only clients were still in widespread use
https://raw.githubusercontent.com/alandipert/ncsa-mosaic/mas...
NCSA Mosaic is the early graphical browser that begat Netscape Navigator that begat Firefox. Later came Internet Explorer, Safari, Chrome and so on
What was the point of spoofing in 1996
Maybe it was just for fun, judging by the examples in the "mosaic-spoof-agents" file
As a matter of practice, by default I do not send a user-agent header. I only send the minumum headers required. For me, that's almost always 1-2 for GET and 4 for POST
For the vast majority of websites I have accessed,^1 this header minimisation has zero effect on the success of the HTTP request
1. For example, I have used a database of sites submitted to HN as way to test if header minimisation affects HTTP request success
Generally I do not use a web browser to make HTTP requests. I perform text processing on the HTML, JSON or whatever is returned, using custom utilities. I store information in SQLite. I read this information as plain text, preferably 7-bit ASCII. I dislike UTF-8
Onlilne debates about "browser fingerprinting" always seem to focus on trying to "blend in", e.g., via "spoofing"
As such, because browser continue to get more bloated with "features", online commenters argue in favor of sending more and more data points to servers that can be used to create a fingerprint instead of reducing the amount of data sent
Because, according to their reasoning (or lack thereof), sending less data would "stand out"
True, but it's generally easier to "spoof" a client that sends less data than one that sends more. And the number of sites that require a user-agent header is still smaller than the number that don't
In the bad old days there were so many differences between html, css and js behaviors that if you wanted your site to be nice you had to change it for the browser. The way css padding worked wasn't even the same. Feature detection was rarely viable for any of this.
No user agent would probably have only entrenched IE6 dominance even more by blocking you from deliberately making a site that works at all on other browsers (including IE7 for that matter)
Yes, fixed point can use simpler hardware. That's also a completely irrelevant consideration for software. The vast majority of processors are optimized for floats now and some operations (e.g. division) are actually faster.
The precision argument also falls apart. Any float with mantissa >= X+Y can get exactly the same results as a QX.Y fixed point. The float will actually perform better across the same range because you have to round it to perform like the fixed point. That means more precision, lower error, automatic normalization, better overflow behavior, a larger working range, etc. And it'll probably be just as fast, unless you're bottlenecked on memory bandwidth of inputs (unlikely). When you inevitably want an exp() or another special function, it's a heck of a lot easier to call libm than implement your own and it will perform better.
Floats are also much easier to get right for your coworkers that aren't numerical analysts.
Pow is famously hard anyway because it's bivariate and there is no currently known way to work around the table-maker's dilemma (TMD). CORE-MATH even crashes upon a new required precision record, because it intentionally avoids Ziv's rounding.
It’s time to sign up with a virtual card, point it at my website, and see how to block them ;)
Machine learning? (on a machine with no WebGL/WebGPU, I guess)
We don't fingerprint for ad purposes, and we destroy PII for humans as fast as we can because PII should be treated as radioactive. But we see customers that are constantly burned by abusive scrapers and the scrapers aren't slowing down.
The current approach to scraping is strip mining the Internet and is having the corresponding pollution effects that you'd expect. I'm fine with individuals doing whatever weird automation they want, more power to you, but it's this industrial scale crawling and extraction that's degrading the Internet from all angles.
OS rendering differences can likely betray you even when canvas extraction is blocked/noised. At least one tor-browser dev has publicly confirmed that you can't even hide the difference between X11 and Wayland[1], nevermind two entirely different OSes.
[1] https://forum.torproject.org/t/linux-is-it-alright-to-run-th...
It's passive surveillance on the order of billions of people. It's not a mom-and-pop shop.
(I have no idea, I don't know too much about this)
If you have that right, the public should have the right to know you're doing this before they enter your store, so they can avoid it.
Same with the websites, they should, legally, have to say they're about to fingerprint you so that you can close your browser tab and never come back.
If you did it in just your store, that wouldn't be a problem. The correct analogy, however, is "why should it be illegal for me to attach a perfectly traceable and invisible air-tag to you when you enter my store, without your explicit consent, and subsequently follow and document your every movement no matter where you go, as long as that location has a business relationship with my store, and also my store is the most popular chain on the planet that has business relationships with basically any relevant business that exists." And I don't think the answer to this one shouldn't be particularly difficult to arrive at.
* Title
* Brief
* Table showing the issue
* Notes about CSS/JS disparity
* An idea for why it happens.
* The idea/code for a solution.
This could just be:
Chromium allows websites to figure out your OS using math functions
Chromium's implementation of Math.tanh and CSS trigonometry functions since version 148 uses the host libc implementation. This is a problem because libc implementations vary between operating systems and can produce slightly different results on different platforms (and potentially even different versions of the same platform). This can be used to accurately distinguish between Linux, MacOS and Windows.
Here's a table showing the issue with Math.tanh:
...
For javascript, so far it's only Math.tanh that uses the system libc, the rest of the functions are implemented in V8. But for CSS it's worse, all functions use the libc versions.
Since math should be deterministic, it would be an improvement to privacy as well as platform uniformity to have V8 implement tanh and have the CSS use those implementations too. <side>Notably there are also some quirks in Web Audio. ...</side>
Here's some code which when compiled with `-ffp-contract=off` will produce the same MacOS result on all the other platforms Chromium supports. One option is to just unify the results, another is to make them match the claimed platform.
...
---
It took me 10 minutes to draft the above. With the effort it probably took to prompt that blog post, you could probably get it proofread, generate the table if you have access to the right boxes but don't have it already. And produce the right code with a test harness, verify it.
It's a lot shorter, but that's because the post doesn't really have that much content. There's huge quantities of fluff, the entire "Four Traps" section is so incoherent that I still don't get what it's trying to say after trying to read it 4 times on two different occasions.
For example, something like "90% of the current internet is behind Cloudflare, Google, Akamai, etc."
The www is only subset of the internet and "90% of the internet" does not use a CDN
In any event, even if "internet" means "www" there is no evidence to support this statement
https://www.netcraft.com/blog/may-2025-web-server-survey
Further, being "behind Cloudflare, Google, Akamai, etc." does not necessarily mean a User-Agent header is required
For example,
https://www.cloudflare.com does not require a UA header
https://example.com is "behind CF" and does not require a UA header
https://www.google.com does not require a UA header
https://developers.akamai.com does not require a UA header
And so on
Individual www users, unless doing mass scanning or the like, probably do not access 100% of the www's sites (according to Netcraft, most are inactive)
More likely, individuals access only a fraction of the active sites
It's unlikely that individuals access exactly the same list of websites^1
For me personally, the vast majority of sites that I access do not require a UA header
The sample set I use as evidence to support comments I make about this on HN is a list of all sites submitted to HN over a recent period.^2 Currently this is 12,641 sites
1. To have a meaningful discussion about UA header requirements, a sample set of sites must be agreed upon
2. Perhaps HN readers access these sites and possibly access some of the same sites. Of course it's also possible that there are HN commenters who do not actually access HN submissions and instead only read comment threads and submit replies
https://news.ycombinator.com does not require a UA header
For example, https://www.apple.com does not require a UA header
For example, http://www.slackware.com does not require a UA header. However, it does require Accept and Accept-Encoding headers
This site can also be accessed over HTTPS
I wish this was the case. Unfortunately, companies that work on non-Chromium browsers need to employ dedicated web compatibility teams to either a) help website users fix non-standard (i.e. Chrome only) HTML/CSS/JS, or b) replicate Chromium-like behaviour for specific (very popular) websites so that they work "correctly".
There's also the websites that deliberately block certain browsers which is what tools like "chrome-mask"[1] are built to solve.
[1] https://addons.mozilla.org/en-GB/firefox/addon/chrome-mask/
They're pretty brutal in blocking non-browsers, I tend to have to add a lot of useless headers.
Not really, there's just only one browser (Chrome). Firefox has declined so low it's a rounding error, and all other browsers are Chrome forks.
The OS doesn't really matter, the amount of entropy it contains is very low. As long as the anonymity set of browser-users is large it's all good. And I believe Tor Browser accomplishes this objective.
If you want to make it harder to guess whether you're human or a malicious bot, you're going to find yourself clicking on more than just traffic lights and bicycles in the near future.
That's just a description of you that I share with my other stores. Casinos, Target, Burger King, etc all do this when you get 86'd, for example.
> Floats are also much easier to get right for your coworkers that aren't numerical analysts.
That one is true, however, when you have people, such as EEs who really care about precision, and know the theory behind it, then floats are often not the obvious choice. It has other advantages, like your calculation running the exact same regardless of CPU and/or compiler, which I'm sure a lot of analysts care about. Afaik finance people don't even use floats for things like account balances, because you can't represent something like 0.1$ exactly.
Fixed point has basically no language support, and is very hard to get right, but sometimes you need to do that.
Do you have any subject matter expertise in quantization errors? Like doing simulations or DSP work? Not trying to be antagonistic, just figure out where you're coming form.
This seems backwards. Hardware is optimized for floats because people use floats. If people used fixed point, hardware would become optimized for that instead.
Given an equal number of transistors, I'm pretty sure fixed point would be a lot faster on equally optimized hardware for almost all operations.
- There’s a well-known way (“Ziv’s rounding”) to get (among other things) a correctly rounded double-precision pow(), but in bad cases it can get slow, meaning really quite slow in practice and we’ve got no idea how slow in the worst case (nobody knows what the worst case is).
- There’s a recent, guaranteed-correct way[1] to get (specifically) a correctly rounded double-precision pow(), but the last step requires “enough” precision and we’ve got no idea how much that actually is, so CORE-MATH uses 256 bits of mantissa and crashes if that turns out not to be enough (no such cases are currently known).
- (Bonus) For most special functions [not just the bivariate ugly duckling of pow()], there’s essentially no hope of getting a correctly rounded quad-precision version anytime soon.
So, I have two dumb questions:
1) Can't the customer rate-limit connections? If the "abuse" in scraping is the number of requests... limit the number of requests?
2) There is probably a market now for federated authentication where the provider gives legal guarantees of anonymized fingerprint in exchange for either payment or selling data. This would provide an identifier for rate-limiting requests, and be pretty much the same as "Sign in with Google", but without identifying the user to the customer site. Are there too many problems with this that make it unfeasible, or would your company/customers be open to such a solution?
Why give up information when you don't have to? Some people disable JS at least some of the time.
Blocking "non-browsers" and blocking IP addresses are two different things, so to speak
The first is based on dumb heuristics and (incorrect) assumptions about behavior based on what software someone is (mistakenly) presumed to be using
The second is based on past behaviour
I send the minimum HTTP headers
I only request what I want; I do not send requests for ads, tracking or telemetry
I accept text formats, e.g., HTML, JSON, etc.
No need for images, CSS, Javascript, etc.
And detecting their platform doesn’t stop you from presenting supplemental links for all platforms. In fact I’d say that’s completely normal.
You get 7 more bits for the most extreme numbers. Which is a good portion of 32, but not crazy. By the time you hit double precision you're only sacrificing 10 of your 64 bits to make your range considerations a hundred times simpler.
In practice when you make your new set of hacks the string you can always evaluate whatever cruft in the useragent today, but next browser shows up.
It does make the user agents insane but I don't know if there's any obviously better system for the problem, even with hindsight
Floating point is generally deterministic in practice with a fairly minor amount of effort, the major remaining issue being library rounding. I actually wrote a library that guarantees this for arbitrary code, with some small, obvious caveats like standard library precision. And the conference talks linked above note, the standard library issues are an increasingly solved problem for modern toolchains. The remaining cases are mostly things you won't do in fixed point. Let me know if you're aware of anyone computing erfc in fixed point for determinism though.
I'm not saying there aren't any situations where other systems are justified, but you probably won't know if you fall into any of them without the kind of numerical analysis that most codebases will never receive.
There are additional methods I chose not to document such as limiting access to logged in accounts that require double-opting-in to acceptable use policies and terms of use, not that most scrapers would give a toss. That it too much whack-a-mole for me personally. That method requires progressively adding friction to account creation and that comes with some pros and cons.
[1] - https://nochan.net/b/Internet-Crap/20260606-How-To-Block-Som...
Step one in doing this is identifying which requests are coming from the same agent. Guess what fingerprinting helps with.
IMO, there is a fundamental difference between (a) not using the data collection, surveillance and advertising-friendly software, i.e., popular graphical web browsers controlled by so-called "tech" companies that, surprise, said so-called "tech" companies and their business partners want people to use and (b) engaging in bad network behaviour, what some used to call poor "netiquette"
The heuristics used to allegedly identify bad behaviour, for example, sending the "wrong" value in a User-Agent header, are beyond stupid
But the false positive totals, "collateral damage", are apparently not large enough to matter
First, a point I didn't make, is that if you have 32 bits of fixed, you get way more precision than with a 32 bit float.
That's true, but I already responded to it. If you step up to the next size of float (e.g. f64), you have more precision than the fixed32. You can do exactly the same computation in f64 with equivalent inputs, and you'll get better precision than doing it in fixed32. Or you can round at every step like fixed does and get a bit-equivalent value if you don't want the precision. It's less memory efficient, but my point is that the remaining use cases for fixed point are exceptional/situational and getting increasingly niche.Maybe using a bigger float type is cheating, but it's basically free because of the ubiquitous support for floats.
In contrast, with floats, you can lose precision.
This only happens with values outside the range of your fixed point type if you use larger floats as mentioned above. I consider that a different argument. You can alternatively view this as the float handling a situation more gracefully than fixed point would have. Afaik finance people don't even use floats for things like account balances, because you can't represent something like 0.1$ exactly.
Finance types typically use decimal types from what I understand. This is really just the result of using a decimal syntax to initialize/output a binary representation. Fixed point has exactly the same problem. Decimals have an analogous issue with the value 1/3. It has other advantages, like your calculation running the exact same regardless of CPU and/or compiler, which I'm sure a lot of analysts care about.
I wrote a library that makes floats more practically deterministic across platforms for very little cost (linked at [0] so you can see the limitations and numbers), and the underlying problem is [maybe] getting a standardized solution in C++29. You can get the same thing today just by changing compiler flags. If you need the special, non-reproducible float functions, your options are mainly to import a library or implement it yourself, same as fixed point. Not trying to be antagonistic, just figure out where you're coming form.
I work in safety-critical automotive/robotics, used to do audio DSP, contributed a bit to the aforementioned standardization, etc. I also have a talk on this topic I've been working on for the last few weeks. It's a bit of a pet subject. Fixed point has basically no language support, and is very hard to get right, but sometimes you need to do that.
There are absolutely situations for it, but that's exactly it: it's situational. And those situations are increasingly uncommon these days, now that hardware with good IEEE support is essentially ubiquitous and compilers/standard libraries are improving their implementations.Hardware floating point on CPUs (including SIMD units) is almost always IEEE 754 compliant these days (excepting only IBM’s weird fantasy land), and there the rules for the non-YOLO operations (+, -, *, /, sqrt, fma) are completely unambiguous and deterministic: treating the inputs as exact, compute the mathematically exact result, then either return it as is if it is exactly representable as a floating-point number, or if it’s not then round it to one that is according to the current rounding mode.
Things that can mess this up:
- GPUs just do whatever they feel like will make them look faster on benchmarks, don’t count on anything.
- Transcendental functions (exp, sin, etc.) are really hard (multiple literal PhDs) to implement according to the rules I’ve just described (“correct rounding”), so you’ve only been able to get such implementations in the last few years, and I believe no stock libm has completely switched so bring your own if you need them.
- Decimal-to-binary and binary-to-decimal conversions, by contrast, are not that hard to implement according to the rules in principle (it’s making them fast that’s difficult), yet Microsoft couldn’t get it right for literal decades, so if you need Windows then double-check CRT versions and bring in well-known open-source conversion code as necessary.
- Denormal inputs or outputs are very slow in some implementations, leading to a hardware option to flush them to zero. Either make sure to not produce them or keep an eye on the option.
- The precise bit pattern of the NaNs you get for invalid inputs may differ across platforms. Either make sure to not produce them (you really shouldn’t) or canonicalize upon de/serialization.
- Sometimes compilers will try to HALP by performing e.g. single-precision math in double-precision accumulators and only rerounding upon store to memory; by fusing * followed by + (two roundings) to hardware fma (only one); by reassociating; etc. Take care to prohibit your compiler from doing these shenanigans (no -ffast-math or -funsafe-math-optimizations ever, in your code or in any dependencies, and God help you if you’re on MSVC).
- Most shamefully, the 8087 (despite spawning the entire IEEE standard in the first place) tried to HALP by using 80-bit registers, so if you need x86-32 then be especially careful with compiler settings (I seem to remember the HALP mode might even be ABI-mandated on some 32-bit platforms so you’ll need to violate that).
The concept of floating point is solid, the IEEE standard is stellar, but the superstructure around it is just—not, requiring an unnecessary amount of vigilance to just make it work as designed.
Yeah, I generally agree with that viewpoint, but you are giving up that information, even without JS.
Your TCP stack can be fingerprinted through long enough observation. Windows, Mac, and Linux all look different on a network level. It's not as simple `if XYZ then OS = Windows`, it's more holistic/probabilistic, but it's possible nonetheless.
e.g. One thing that bugs me about arch linux is that they recently changed the kernel default TCP keepalive time to be shorter (much shorter[1]), making arch users stand out a lot. So, not only can a fingerprinter identify your OS, they might be able to pin down your exact distro based on TCP behavior alone.
I guess my point is that hiding the OS would be a massive amount of effort to plug a hole that cannot be plugged without effectively controlling the entire OS. TB/MVB is limited in what it can do by itself.
Fair. Scrapers should be polite and do their utmost to consume the smallest possible amount of resources.
> without my consent by masking your user-agent
Your consent is not required. It's my user agent. I set it to whatever I want.
> for the purpose of stealing data I didn't authorize you to have
Data can't be "stolen", only copied.
You set up an HTTP server that literally sends people the data when they request it. Don't do that if you don't want people to have the data. Secrecy is the only possible defense.
It's like public photography, it's intrinsically legal, except when it's a Flock camera and then it's suddenly an invasion of privacy.
Bad guys use a botnet (compromised residential computers and routers) to defeat rate limiting and ip-based blocking.
The moment you openly publish information on the Internet, you have already given consent. There are other solutions to bandwidth usage.
User-agent discrimination should be illegal. All it does is further the control that Big Tech has, and help authoritarian governments with their control too.
Fingerprinting is usually about canvas, WebGL, fonts, audio. There is a quieter signal, and it lives in the last bits of a number.
Run this in any console:
Math.tanh(0.8)
// 0.6640367702678491 genuine Linux Chrome (glibc)
// 0.664036770267849 genuine macOS Chrome (libsystem_m)
// 0.6640367702678489 genuine Windows Chrome (UCRT)
That output is an approximation, and its exact bits depend on the OS that computed it. A genuine Mac runs Math.tanh through Apple’s math library. Linux runs it through glibc1. The two disagree on about a quarter of all inputs, usually by one unit in the last place (1 ULP2). Windows, through the Universal C Runtime, disagrees with both on a few percent, and on the input above all three land on a different bit.
The same call, run on genuine Chrome 150 across three real machines:
| Call | Linux (glibc) | macOS (libsystem_m) |
Windows (UCRT) | Split |
|---|---|---|---|---|
Math.tanh(0.5) |
0.46211715726000974 |
0.46211715726000974 |
0.46211715726000974 |
all three agree |
Math.tanh(0.7) |
0.6043677771171636 |
0.6043677771171635 |
0.6043677771171635 |
Linux alone, 1 ULP |
Math.tanh(0.8) |
0.6640367702678491 |
0.664036770267849 |
0.6640367702678489 |
all three differ, 2 ULP spread |
Math.tanh(0.9) |
0.7162978701990245 |
0.7162978701990245 |
0.7162978701990244 |
Windows alone, 1 ULP |
Measured over the DevTools protocol on Chrome 150: Linux (glibc), macOS 26 on Apple Silicon (libsystem_m), Windows 11 (ucrtbase.dll). tanh(0.5) is one of the roughly three-in-four inputs where everyone agrees, which is exactly why it makes a useless probe. tanh(0.8) is one that separates all three at once.
One tanh call on the right input is a per-OS signature. Claim macOS, return Linux math bits, and you have contradicted your own User-Agent.
This tell is recent. Until Chrome 148, V8 computed tanh itself with a bundled fdlibm3 port, so it returned the same bits on every OS and leaked nothing. V8 commit c1486295ae5 replaced it with std::tanh, which reads the host libm. It first shipped in V8 14.8.57, which is Chrome 148. Chrome 147 and earlier do not leak here. Chrome 148, 149, and 150 do.
Scrapfly ships a browser that has to match a real one across hundreds of signals, and math is one of the harder ones.
IEEE 7544 defines how a double is stored. It does not require sin, cos, tanh, or exp to be correctly rounded. Correct rounding is expensive, so every vendor ships a libm5 that trades a fraction of a ULP for speed, with its own minimax6 coefficients, lookup tables, and reduction constants.
The three implementations produce three sets of bits:
libsystem_mucrtbase.dll)They agree almost everywhere and split just often enough to classify the OS. A detector needs no math, only a table: genuine macOS Chrome returns one pattern for cos(1), genuine Linux Chrome returns another, and a single comparison tells them apart.
“Just reimplement the Mac functions” breaks on contact, for four reasons.
1. Only some math leaks. V88 ships its own math and links it statically: Math.exp, Math.pow, Math.atan, and most of the rest come from bundled llvm-libc9, and Math.sin / Math.cos from a bundled glibc-derived dbl-64 routine. All of them are identical on every OS, so spoofing them creates an inconsistency. The exception is Math.tanh: since Chrome 148 V8 computes it with the platform std::tanh instead of the bundled routine it used before, so it now reads the host libm. It is the only Math.* that leaks the OS, and that asymmetry is itself checkable.
2. JavaScript math and CSS math are different code paths. CSS sin(), cos(), and atan2() do not share code with Math.sin. The layout engine reduces the angle in degrees, then calls platform std::sin on the reduced value. That gives a different result than a direct radian sin(), and it hits the host libm, so all seven CSS trig functions leak. We reproduced the degree reduction and the radians-to-degrees step bit-for-bit, not just the leaf function.
3. macOS has two math libraries that disagree. Apple Silicon carries scalar libsystem_m and the Accelerate framework10’s vector routines (vvsin, vvtanh). They are different code. Across a million inputs they diverge on 10 to 89 percent, depending on the function. Take cos(0): scalar returns exactly 1.0, Accelerate returns 0.9999999999999999. So “reproduce Apple’s math” is undefined until you know which library the browser calls, at which site. We resolved it by driving real Chrome on a real Mac over the debugging protocol and reading the exact double. Answer: scalar libsystem_m backs Math.tanh, CSS trig, and the audio compressor’s per-sample transcendentals. Accelerate backs Chrome’s Web Audio DSP on Mac, the FFT, the vector math, and the biquad filters (fft_frame_mac.cc, vector_math_mac.h, biquad.cc, all BUILDFLAG(IS_MAC)). Pick the wrong library for a given call site and you land 1 ULP off on most inputs, worse than not spoofing.
4. Architecture leaks. ARM and x86 differ on fused-multiply-add and on NaN sign propagation. A reproduction that is correct on paper drifts if the compiler fuses a multiply-add on one target and not the other.
Put the routing on one page. Bold is the host libm (glibc, Apple libsystem_m, or UCRT), the code that leaks the OS. Everything else is identical on every machine and safe to leave alone.
| Operation | V8 Math.* (JS) |
CSS calc() |
Web Audio |
|---|---|---|---|
sin cos tan |
V8 bundled | host libm | Accelerate (osc FFT), sin scalar in the compressor |
asin acos atan atan2 |
V8 bundled | host libm | not used |
tanh |
host libm | none | not used |
exp |
V8 bundled | host libm | scalar in the compressor |
log log2 log10 pow |
V8 bundled | host libm | scalar log10f / powf in the compressor |
| vector add/mul/scale, FFT | n/a | n/a | Accelerate (vDSP) on Mac |
sqrt abs + - * / |
hardware | hardware | hardware |
V8 bundled = statically linked and identical on every OS: llvm-libc for most functions, a glibc-derived dbl-64 routine for sin/cos. host libm = the platform library that leaks the OS (libsystem_m on Mac, glibc on Linux, UCRT on Windows). Accelerate = Apple’s vDSP, which Chrome uses for the Mac Web Audio DSP.
V8 routes almost everything through its own bundled math, so JavaScript Math is a tell in exactly one place, Math.tanh. CSS is a tell everywhere, because Blink calls the host libm directly for every trig function. Web Audio on Mac runs on Accelerate for the FFT and the vector stages, while the DynamicsCompressor’s per-sample transcendentals stay scalar libsystem_m, so one audio graph touches three separate libraries.
WASM is not in the table because it has no transcendental opcodes. sin and friends come from whatever libm the module bundled, and its arithmetic (f64.sqrt, f64.mul) is hardware, so WASM math is identical on every OS. Its only fingerprint axis is the ARM-versus-x86 split in NaN canonicalization and a few SIMD roundings.
The tells cluster in three surfaces: Math.tanh, every CSS trig function, and Web Audio, where the Accelerate FFT carries the CPU architecture and the compressor’s scalar libsystem_m carries the OS.
No noise. Perturbing the output fails twice. A reference comparison sees a value that matches no real OS, and per-call randomness breaks determinism, which is its own tell. The target is a value identical to the OS you claim, which noise cannot produce.
Reproduce the algorithm exactly. Recover the target’s minimax coefficients, exponent tables, and reduction constants from its libm, and transcribe them to portable C. Match every bit, including the inputs where the target rounds the wrong way. Here is Apple’s sin polynomial, coefficients pulled straight out of libsystem_m:
// Every fused multiply-add Apple emits is written as an explicit fma(). The
// bit pattern of each coefficient is copied verbatim; a decimal transcription
// would round differently.
static const double P[6] = {
0x1.5d8fd1fd19ccdp-33, -0x1.ae5e5a9291f5dp-26, 0x1.71de3567d48a1p-19,
-0x1.a01a019bfdf03p-13, 0x1.111111110f7d0p-7, -0x1.5555555555548p-3,
};
static double sin_poly(double x2) {
double p = fma(x2, P[0], P[1]);
p = fma(x2, p, P[2]);
p = fma(x2, p, P[3]);
p = fma(x2, p, P[4]);
p = fma(x2, p, P[5]);
return x2 * p; // caller finishes: sin(x) = fma(x, x2*p, x)
}
Make it deterministic. That explicit fma() matters. Compile with FMA contraction off (-ffp-contract=off) so the compiler never invents or drops a fusion of its own. Now the fused ops are exactly the ones Apple fuses, and the result is identical on FMA11 and non-FMA CPUs, and identical between the ARM machine you imitate and the x86 fleet you run on. Hardware FMA and correctly-rounded software FMA return the same bits.
When reproduction is not worth it, lift the original. Windows UCRT is x86-64, the same ISA as a Linux server, and position-independent. Map the genuine ucrtbase.dll into memory at runtime and call its exports directly. The bits are genuine because the code is genuine, no reverse-engineering required.
Calling into Windows code from a Linux binary hits the ABI boundary. UCRT is compiled for the Windows x64 convention: the callee owns 32 bytes of shadow space above the return address, and the callee-saved register set differs from System V. Declare the function pointers ms_abi or clang’s frame layout gets corrupted by the callee’s shadow-space writes, and the indirect call jumps into garbage.
// Windows x64 ABI, not System V. Without ms_abi the call crashes.
typedef double(__attribute__((ms_abi)) * D1)(double); // tanh, sin, ...
typedef double(__attribute__((ms_abi)) * D2)(double, double); // atan2
// The mapped DLL code is not a CFI-registered indirect-call target, so
// -fsanitize=cfi-icall (on in production) #UD-traps every call -> SIGILL at
// startup. Opt the wrappers that call through the pointers out of that check.
[[clang::no_sanitize("cfi-icall")]]
double ucrt_tanh(double x) {
return ucrt.loaded ? ucrt.tanh(x) : std::tanh(x);
}
One more detail decides correctness. Every UCRT math function starts with mov eax, [rip+disp32], reading a CPU-dispatch flag that selects the scalar or the FMA/AVX2 code path. A fresh mapping leaves it at zero, so you get the slow path, whose bits differ from what a modern Windows box produces. Extract the flag’s address from the tanh prologue and force it to the FMA path before the first call, and the lifted library matches a real Windows machine bit-for-bit.
Patch the chokepoint, gate it. Hook the single function that owns the value, where the engine calls libm. Gate on the claimed OS: Linux keeps glibc, Mac gets the reproduction.
Watch the clock. A perfect reproduction that runs slow is still a tell. Our first build lowered every fma() to a software call, because the default x86 baseline predates hardware FMA. That ran 2.5 to 6 times slower than native. A loop timing Math.tanh against Math.sin would show a ratio no real browser has. Turning on hardware FMA cut each fused op to one instruction: about 6 times faster, faster than glibc, and bit-identical.
None of this ships without proof. Our harness runs 871,000 inputs per release across every branch and domain: dense grids, interval boundaries, subnormals, signed zeros, infinities, NaNs. Two ground truths back it:
Math.tanh and every CSS trig function at full precision. This is the surface a fingerprinter reads.We ship at bit-for-bit parity with genuine Mac Chrome on Math.tanh and on CSS sin, cos, tan, asin, acos, atan, and atan2, with the reproduction verified identical to the machine code in the shipped binary. Domain edges get checked too: asin(2) on a real Mac resolves to 0 (out of domain is NaN, and CSS clamps NaN to zero), not the 90 degrees a naive reproduction returns.
Math is deterministic, cheap to probe, hard to fake, and almost never on a spoofing stack’s radar. That makes it a strong signal for a defender and a liability for a scraper. Getting it right takes reverse-engineering vendor libm internals, mapping how three engines route math per call site, matching algorithms to the last bit, holding determinism across architectures, and proving it against real hardware.
Scrapfly’s browser carries all of it. Send a request through our API and ask to present as macOS, and the identity holds down to the rounding of a cosine.
Scrapium is Scrapfly’s scraping browser, built to stay indistinguishable from real traffic. Our engineering blog goes deep on the rest of the stack.
glibc: the GNU C Library, the standard C library and libm on most Linux systems. Reference: gnu.org/software/libc. ↩︎
ULP (unit in the last place): the gap between two consecutive representable floating-point numbers at a given magnitude. “1 ULP off” is the smallest difference a double can express. Reference: Wikipedia. ↩︎
fdlibm: Sun Microsystems’ “Freely Distributable libm”, the portable reference implementation of the C math functions. V8 carried a port of it and computed Math.tanh with it until Chrome 148, returning identical bits on every OS. Reference: netlib fdlibm. ↩︎
IEEE 754: the floating-point standard. It fixes how a double is stored but does not require transcendental functions to be correctly rounded, which is the room every vendor’s libm fills differently. Reference: Wikipedia. ↩︎
libm: the C standard library’s math module (sin, cos, exp, tanh, and so on). Each OS ships its own build, which is why the same call returns different bits: glibc on Linux, Apple libsystem_m on macOS, UCRT on Windows. Reference: C mathematical functions. ↩︎
minimax polynomial: the approximating polynomial that minimizes worst-case error. Each libm picks its own coefficients, and that choice is where the differing bits originate. Reference: Wikipedia. ↩︎
UCRT (Universal C Runtime): Microsoft’s C runtime (ucrtbase.dll) and its math functions. Being x86-64 and position-independent lets it be mapped into a Linux process and called directly. Reference: Microsoft Learn. ↩︎
V8 / Blink: Chrome’s JavaScript engine (routes Math.* through llvm-libc) and Chromium’s rendering engine (owns CSS calc() trig, which calls the host libm directly). References: v8.dev, chromium.org/blink. ↩︎
llvm-libc: LLVM’s C library. V8 statically links its math routines, so Math.sin, cos, exp, and pow are identical on every OS, which is why only Math.tanh leaks. Reference: libc.llvm.org. ↩︎
Accelerate framework: Apple’s vector and DSP library (vvsin, vvtanh, vDSP). It backs only the Web Audio FFT, not the scalar Math.* calls, which is why the scalar and vector results disagree. Reference: Apple Developer. ↩︎
FMA (fused multiply-add): a single instruction computing a*b+c with one rounding instead of two. Compiling -ffp-contract=off stops the compiler adding or dropping fusions, which is what makes the reproduction bit-stable across CPUs. Reference: Wikipedia. ↩︎
- Transcendental functions (exp, sin, etc.) are really hard (multiple literal PhDs) to implement according to the rules I’ve just described (“correct rounding”), so you’ve only been able to get such implementations in the last few years, and I believe no stock libm has completely switched so bring your own if you need them.
This is right, but it's useful to point out how important the "correct rounding" qualification is to the difficulty of the problem. Writing a "good enough" function with floats is easy even for non-experts. exp can be efficiently implemented in hardware with a 4 element lookup table and polynomial interpolation. Sin/Cos are range reduction + a minimax polynomial from sollya. But the standard [rightly] doesn't prescribe specific implementations, so you need fully correct rounding to have cross-platform determinism.Correct transcendentals are also difficult in fixed point. When pigweed was implementing them for their fixed impl, they got help from from an ex. Mathworks floating point expert on the LLVM team with a relevant PhD to do it correctly [0].
There's been some pushing for browsers to automatically decide what language you want to view a webpage in, and that is even more insane than the User-Agent header is. The correct solution for content that exists in multiple languages is to make each language available at its own URL. Just like I already said. Why in the world would you want to lose the option to select the language you want?
Couldn't agree more. Nonsense like remote attestation too. If this discrimination persists, it will lead to the destruction of what little computing freedom we still have.
If these scraping companies would respect some kind of X-Scraping-Permitted header, this whole process would be a lot better, but that's not going to happen in an industry that has already normalized using botnets.
JS devs were kinda able to patch around the nonsense because they were able to feature-detect - part of the reason this stuck around was because no legitimate user or dev cared (or should care). But the header was mostly (useless) noise, and the people spoofing were dealing with the couple bad apples of the time.
Of course, defining features is easier said than done, and a standards body is a challenging environment to define these in...
I get why people are fingerprinting bots and others are working around it, but neither are "legitimate" applications - if your content is public, it's public, end of story. And working around these controls to sell botnet access to sites is equally illegitimate - nobody has a right to resell content they do not own...
By the same argument I could say: If I send you an exploit and you execute it, don’t complain that your setup fell for it. Just don’t download and run random data from the internet.
In reality there’s a consent and expectation beyond the pure technicals.
"If you didn't want me to do this, you should had a fence/cameras/security guards. You shouldn't have dressed like that. You shouldn't have put your phone in that pocket."
Excusing trillion dollar corporations like low level criminals is embarrassing. Society shouldn't have to lock itself up because bad actors are spreading everywhere. The bad actors should just be removed.
Though, scrapers can certainly steal capacity through conversion for their own use. When they do so, they permanently deprive the site owner and other users the beneficial use of that capacity at that time.
I'm speaking morally, not legally. Though, in the US at least, there are parallels with free newspapers. You're allowed to take one for free. It's not legal to clear the whole rack.
How does that apply to Tor? It uses custom packets. The entry node may be able to obtain a TCP fingerprint but that's all.
To be clear I'm not anti-float, as they have less surprising behavior than fixed point, and are much less fiddly, but I do have to note that f32 sits at that awkward spot where it's not accurate enough for a lot of numerical work, but stepping up to f64 carries a significant performance penalty on things like GPUs, and most DSPs don't really give you f64 hardware at full rate, if at all.
In contrast, 32 bit fixed point gives you 6 extra bits of precision, (IEEE754 mantissa should count as 26 bit), which can often be the saving grace.
For example, in video games, if you mandate a 0.01mm precision, with int32, you get a 40kmX40km area, which is plenty, and with float32, you have to divide every dimension by 64, which is not enough for even mid-sized maps, and you have to employ tricks or go straight to f64.
If you stick to the safe bits I've been discussing elsewhere in this thread and your platforms implement IEEE floats, float math will also be commutative + associative and you won't have to deal with precision loss. That means your usable range will be narrower than the same size fixed type (because it's limited by the mantissa) but it's large enough to still be useful.
Here's some stuff on Adversarial Interoperability, which is an incredibly good thing that can reverse enshittification if it becomes more widespread: https://www.eff.org/deeplinks/2019/10/adversarial-interopera...
You're right about that particular constraint, though I'd question why the achievable 2-4mm precision at 32 or 64km are meaningfully different. Covering up unstable collision code and adaptive methods would be a rewrite?
I don't. I go out of my way to filter everything. Scraping is but one of the tools I use to do it. I want just the data that I actually care about, not people's javascripted hot mess websites full of malware-vectoring ads, fingerprinting and tracking.
I don't let my computers talk to strangers either. My servers don't respond to just anyone, they only reply to me, and only after I've cryptographically authenticated. When others try to talk to them it's like they're not even there.
But people want their computers to talk to strangers, don't they? They want to serve pages and pages of ads to massive audiences. Unlike your exploitation example, nobody's actively invading their computers and exfiltrating data. Breaking into someone else's computers and dumping their private databases is one thing. We're just requesting the exact same data that they're more than happy to send out to literally anyone who shows up with a browser, through the exact same channels even. So I really have no sympathy.