Open Bug 1873382 Opened 10 months ago Updated 13 days ago

under resistFingerprinting, media query matches is always false

Categories

(Core :: CSS Parsing and Computation, defect)

Firefox 122
defect

Tracking

()

ASSIGNED

People

(Reporter: mike, Assigned: fkilic)

References

(Blocks 1 open bug)

Details

Attachments

(4 files)

User Agent: Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0

Steps to reproduce:

actual version: Firefox Developer Edition 122.0b4

At the console, I ran
window.matchMedia('(resolution: ' + window.devicePixelRatio + 'dppx)')

both with privacy.resistFingerprinting on and off.

Actual results:

with privacy.resistFingerprinting off:

window.devicePixelRatio
<- 1.2
window.matchMedia('(resolution: ' + window.devicePixelRatio + 'dppx)')
<- MediaQueryList { media: "(resolution: 1.2dppx)", matches: true, onchange: null }

With it on:

window.devicePixelRatio
<- 1
window.matchMedia('(resolution: ' + window.devicePixelRatio + 'dppx)')
<- MediaQueryList { media: "(resolution: 1dppx)", matches: false, onchange: null }

Expected results:

The returned MediaQueryList field 'matches' should always be true, even if firefox lies about the resolution for privacy reasons.

The Bugbug bot thinks this bug should belong to the 'Core::Privacy: Anti-Tracking' component, and is moving the bug to that component. Please correct in case you think the bot is wrong.

Component: Untriaged → Privacy: Anti-Tracking
Product: Firefox → Core

So nsGlobalWindowInner::GetDevicePixelRatio returns 1.0 when resistFingerprinting is on, but Gecko_MediaFeatures_GetResolution returns pc->DeviceContext()->GetFullZoom(). Should those match or is the assumption in comment 0 wrong?

Component: Privacy: Anti-Tracking → CSS Parsing and Computation
Flags: needinfo?(emilio)

Yeah I think they ought to match.

Flags: needinfo?(emilio)

RFP protects all those by only returning the zoom level

Severity: -- → S3
Assignee: nobody → fkilic
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

Is the zoom level something we want to leak? It was being leaked already (you can brute-force matchMedia('(resolution: <some value here>dppx)') to get user's zoom level), so I built the patches around that fact, but if we want to hide it, then we would have to do something else (changing even the current behaviour).

Flags: needinfo?(thorin)
Attached image example.png

where anotated with [✓ RFP], these are all protected to only return the "zoom" level (except window.devicePixelRatio which returns 2). Without RFP, "zoom" is a calculation and a best guess at that - it's not something that can be looked up or relied on - for example system scaling will affect the measurements !== "zoom". RFP also ignores any site specific zoom levels, and always resets to 100% zoom on new tabs and new eTLD+1s etc. If a user zooms after that, big deal, the protected values are still protected (i.e everyone would be the same assuming the same zoom levels apply .. 100, 110 etc)

We haven't patched all possible methods, but we can certainly make it harder. Subpixel entropy (which is effectively what this is), manifests almost everywhere measurements are made, but we think we may have a plan for that - "a concept of a plan" - which involves jittering zoom :) early days

It was being leaked already (you can brute-force matchMedia('(resolution: <some value here>dppx)') to get user's zoom level)

Unless I am missing something, the matchMedia lines and the first two values on the devicePixelRatio line (so total of five values) in the image are already doing that: a binary search using a lamba and two properties: excuse my code

So AFAIK, there is no leak in matchMedia

so I built the patches around that fact

but are we leaking? see above. Also just because we can't (yet) cover all methods (the subpixel problem) doesn't mean the current protection isn't worth it. It's not a zero sum game.

I fail to see what multiplying the zoom level x 2 (our spoofed value) will achieve that leaving it at x 1 already does. The value 2 for dPR is a recent change. If you mean multiplying by the real dPR value then we lose our protection - and that's exactly what we're protecting against - leaking dpi/dPR

ni: piero, maybe he can explain it better than me

Flags: needinfo?(thorin) → needinfo?(pierov)

(In reply to Fatih Kilic from comment #8)

Is the zoom level something we want to leak? It was being leaked already (you can brute-force matchMedia('(resolution: <some value here>dppx)') to get user's zoom level), so I built the patches around that fact, but if we want to hide it, then we would have to do something else (changing even the current behaviour).

Yes, that is indeed possible. You won't get a direct value, but you can get close to one (e.g., with a bisection like Thorin said).
Notice that it will leak also subpixels (e.g., layout.css.devPixelsPerPx, or system-wide scaling), because of some roundings that happen when converting between device pixels and app units.
E.g., for 150% zoom and layout.css.devPixelsPerPx set to 1.2 you get something around 1.5151515 +- ε with matchMedia(resolution > {value}dppx).

We don't care much about zoom, as it'll be isolated, reset, and there isn't a real way to prevent measuring it indirectly, if one really wanted to.
We'd like to do something about the subpixels, but it's a very hard problem.

Flags: needinfo?(pierov)

Yes, that is indeed possible

Just to be clear - it's not leaking dpi or dPR (unless I am missing something). At 100% zoom everyone is identical. Outside of 100% zoom, subpixels may manifest (probably, can't confirm off the top of my head) but the measurements are still based on our "hardcoded" 100% zoom levels

(In reply to Thorin [:thorin] from comment #11)

Yes, that is indeed possible

Just to be clear - it's not leaking dpi or dPR (unless I am missing something). At 100% zoom everyone is identical. Outside of 100% zoom, subpixels may manifest (probably, can't confirm off the top of my head) but the measurements are still based on our "hardcoded" 100% zoom levels

To be more precise, with RFP the resolution matches the "full zoom", which is computed for example when the app units per dev pixel change.
Since it's computed with a round, the full zoom matches the zoom value the user sees only for some zoom values (such as 100%) or when using neat scaling levels (such as 1, 1.5 or 2).
In most of the other cases, subpixels will affect this value, but you won't be able to get the exact scaling level (system scaling, devPixelsPerPx, etc...) reliably from that.

but are we leaking? see above. Also just because we can't (yet) cover all methods (the subpixel problem) doesn't mean the current protection isn't worth it. It's not a zero sum game.

I mean it is not leak if you don't consider it a leak ahhaha. that was basically my question, do we care about whether user zoomed or not.

I fail to see what multiplying the zoom level x 2 (our spoofed value) will achieve that leaving it at x 1 already does

This will just match properties. For example, if devicePixeRatio is 2, then dpi should be 96*2. Zooming changes mAppUnitsPerDevPixel, which is then used everywhere to compute these values, including devicePixelRatio.

If you have a devicePixelRatio=2 device, go to TZP.html in non-RFP and note the values you got for those CSS properties you shared in the image, and then turn on RFP repeat the same steps, the only thing not changing would be devicePixelRatio, which doesn't make sense. the other properties listed there should be multiplied by the device pixel ratio, and in our case that's 2. So, this patch will only match those broken properties.

Since it's computed with a round, the full zoom matches the zoom value the user sees only for some zoom values (such as 100%) or when using neat scaling levels (such as 1, 1.5 or 2).

Is it really important to get super precise though? With no RFP, at 110% on a normally 2 devicePixelRatio device, I get 2.2222222222222223 for dPR and 2.222222328186035 for the estimate, at 120%, I get 2.4 for dPR and 2.4000000953674316 for the estimate. I think it is already pretty close.

So if this patch lands, we will expose the exact values being estimated here, but it is already pretty precise I think?

Other than that, just multiplying the CSS properties by 2 shouldn't really affect anything, at best it should make websites work better. Looking at the source code,

  • nsGlobalWindowInner::GetDevicePixelRatio is only used for getting window.devicePixelRatio
  • Gecko_MediaFeatures_GetResolution is only used by eval_resolution and that is only used by resolution, -moz-device-pixel-ratio, and -webkit-{min|max}-device-pixel-ratio media queries.

So if this patch lands, we will expose the exact values being estimated here, but it is already pretty precise I think?

Yes, the patches don't add any additional information.
They look good to me.

OP's STR are a little confusing. Nine months ago we used a hardcoded 1 for dpr, and all the matchMedia results returned the equivalent. The problem only arose when the user zoomed off 100%, because all the other values would change, but window.devicePixelRatio didn't change. So the problem was one we didn't care about - when users are not at 100% zoom. It is not clear from OP that the user was zoomed. So the original issue was a non-issue IMO (because TB uses 100% zoom on all new eTLD+1's and new tabs).

But now we hardcode at 2, the default matchMedia etc values don't match (at 100% zoom). So now we want to x 2 it all so they do match [1]. This is fine. If we're going to always multiple by 2 then this does not solve the issue of when the user zooms - which again, is not an issue TB cares about. Otherwise the real solution would be to multiple dpr (hardcoded at 2) by zoom and then use that to multiple matchMedia

Unless I'm missing something, this is not addressing the original issue, but does address one that has come up since (namely changing to hardcoded 2)

[1] Make sure -webkit is rounded down to it's few results or you will look weird

I wonder if there's much actual value in spoofing the devicePixelRatio at all here?

It's pretty-trivially sniffable using JS, based on how border-snapping works (which depends on and hence exposes the relationship between CSS pixels and border-pixels): https://drafts.csswg.org/css-values-4/#snap-a-length-as-a-border-width

Here's a proof-of-concept to demonstrate that. When loaded, it prints out the observed window.devicePixelRatio and also an inferred one based on border sizing measurements.

On my (non-HiDPI) display, when I zoom to these percentages, I get these inferred devicePixelRatios:

120%: 1.2048192771084338
133%: 1.3333333333333333
200%: 2
300%: 3.0303030303030303

(I haven't tried but I'm pretty sure you could build a similar PoC using only CSS, with CSS container queries, where e.g. a background-image depends on the available space in a container, and the container has a fractional-valued border width that gets snapped to an exact number of device pixels.)

daniel ... a border PoC has been in TZP since jesus, just saying .. https://arkenfox.github.io/TZP/tzp.html#screen - second to last item in the screen section. Note, this is not a zero sum game - making scripts use the dom and elements as a method is a bonus. Maybe we'll close those loops in the future.

Also your PoC returns 2 for me with RFP on, but on TZP, I still get my real value of 1. Not sure if your PoC works, unless I've misunderstood what it does doesn't output the inferred value when viewed as an attachment :-(

(In reply to Thorin [:thorin] from comment #18)

daniel ... a border PoC has been in TZP since jesus, just saying .. https://arkenfox.github.io/TZP/tzp.html#screen
[...]

Thanks; that does seem like it's the same thing I'm talking about, yeah.

My hypothetical CSS-only container-query-style approach in comment 17 is probably not in TZP (since container queries are relatively recent). Writing a PoC for that would be a small amount of work, but it's quite possible to do so in theory.

Note, this is not a zero sum game - making scripts use the dom and elements as a method is a bonus. Maybe we'll close those loops in the future.

Right. Just wanted to be sure that the existing hurdle's bypassability is considered, when evaluating the benefits against the costs, with e.g. the site-breakage referenced in comment 1 being an example of a cost. (I know the cost-vs-benefit judgement is fuzzy as with all rFP-style interventions; and maybe the benefit is indeed worth it in this case; I'm not sure.)

Also your PoC ~~returns 2 for me with RFP on, but on TZP, I still get my real value of 1. Not sure if your PoC works,

Yeah, sorry -- I should've said, my PoC only tries to output an inferred value if your true devicePixelRatio is greater than 1. That was just laziness on my part in writing the PoC. If your true devicePixelRatio is 1 and you want to try the attached PoC, you can try applying full-page-zoom which should make the logic activate and start printing out an inferred devicePixelRatio (which would then be your full-page-zoom zoom-factor; full-page-zoom is essentially the same sort of scaling as HiDPI scaling, under the hood).

doesn't output the inferred value when viewed as an attachment :-(

FWIW I'm not seeing any difference in behavior when viewing it as an attachment vs. not; but I don't want to sidetrack on PoC diagnosis too much here so I won't worry about this. :)

You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: