Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webgl #234

Open
Thorin-Oakenpants opened this issue Apr 3, 2023 · 25 comments
Open

webgl #234

Thorin-Oakenpants opened this issue Apr 3, 2023 · 25 comments
Labels

Comments

@Thorin-Oakenpants
Copy link
Contributor

@abrahamjuliot - is this uptodate? https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2

I don't want to re-invent the wheel, so want to use your code (accredited) if that's OK - please advise

ATM, I only want to collect the parameters, extensions, vendor, etc - not any actual rendering


webgl

so some questions

Q1

https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2#file-webgl-js-L91-L94

        let canvas = {}
        let context = {}
        try {
            canvas = document.createElement('canvas')
            context = canvas.getContext(type) || canvas.getContext('experimental-' + type)

so here, I wanted to use an already created canvas (I think it saves time)

        let canvas = {}
	let context = {}
	try {
		canvas = dom.webglcanvas
		context = canvas.getContext(type) || canvas.getContext('experimental-' + type)

but I get errors for webgl2 (webgl is fine)

errors

I also don't see where you remove and cleanup the document.createElement('canvas') - remember, TZP allows section reruns so I don't want to end up with dozens of canvas elements


Q2

what is newmethods meant to signify in webgl2 ?


Q3

for data collection/entropy - I am not webgl savvy - but remember in the old TZP repo we discussed, and I mocked up, a webgl section that would return webgl, webgl2, experimental results - a bit like

how is this all reflected in the objects in the console? I'm a bit lost


Q4

how/what are you doing with this data to only show one set of parameters on creepy - I can see that some are identical in webgl 1 vs 2 - and there is a diffs output. Are you merging these?


Q5?

I guess I'll have to play to catch errors and return that - you seem to just console them and return undefined (I guess I could do that)

@Thorin-Oakenpants
Copy link
Contributor Author

Q1 - answered: I just needed two canvas elements

		<div>
			<canvas id="webglcanvas"></canvas>
			<canvas id="webgl2canvas"></canvas>
		</div>

is there some benefit to creating the canvas on the fly?

@abrahamjuliot
Copy link
Collaborator

The gist is good. Always welcome... All credit for that one goes to https://github.com/CesiumGS/webglreport. My refactoring is minimal.

There are a few techniques not in there, but worth checking out. These are mostly experimental ideas that can be used to determine trustworthy vs. suspicious GPUs.

// Highlight common GPU brands
// gpu can be a string of the renderer or both the vendor and renderer
function getGpuBrand(gpu) {
  if (!gpu) return
  const gpuBrandMatcher = /(adreno|amd|apple|intel|llvm|mali|microsoft|nvidia|parallels|powervr|samsung|swiftshader|virtualbox|vmware)/i

  const brand = (
    /radeon/i.test(gpu) ? 'AMD' :
    /geforce/i.test(gpu) ? 'NVIDIA' :
    ( (gpuBrandMatcher.exec(gpu) || [])[0] || 'Other' )
  )

  return brand
}

// Takes the parameter object and generate a fingerprint of sorted numeric values
// This can be used to create a known good hash table and known brands can be labelled for each hash.
function generateParamFingerprint(parameters) {
  if (!parameters) return
  return '' + [
    ...new Set(Object.values(parameters)
      .filter((val) => val && typeof val != 'string')
      .flat()
      .map((val) => Number(val))),
  ].sort((a, b) => (a - b))
}

@abrahamjuliot
Copy link
Collaborator

Q1

A premade canvas in the HTML can sometimes bypass scripts observing createElement('canvas'). Optionally, one context is good. webgl2 has more parameters. Since we are not appending the element to the page, a new document.createElement('canvas') on repeat re-runs should be fine.

@abrahamjuliot
Copy link
Collaborator

Q2

newMethods is just functions in WebGLRenderingContext.prototype. It's not that great for fingerprinting (compared to CSS and the Window). It just current browser features in the prototype.

@abrahamjuliot
Copy link
Collaborator

Q3

I forgot to add the sections like "Vertex Shader" and "Rasterizer", but these can be determined based on the property names. We just need to create a map.

parameterType = {
  'Vertex Shader': [
     'MAX_VERTEX_ATTRIBS',
     'MAX_VERTEX_TEXTURE_IMAGE_UNITS',
     'MAX_VERTEX_UNIFORM_VECTORS',
  ]
}

if (ParameterType['Vertex Shader'].includes(propertyName)) {
  // add to Vertex Shader section
}

@abrahamjuliot
Copy link
Collaborator

Q4

I merge as a final simplification, but also check and flag mismatch behind the scenes. I recall an incident where a mismatch was valid, so I just treat it as questionable.

@abrahamjuliot
Copy link
Collaborator

abrahamjuliot commented Apr 4, 2023

Q5

We can trap the error and push to a global collection. There are a few places different errors might get thrown due to users disabling or blocking the API, or parts of the API.

@Thorin-Oakenpants
Copy link
Contributor Author

I merge as a final simplification

hmmm, so I'm not sure how I want to present all this info - where is all the experimental stuff? I mean we can see webgl and webgl2. e.g. here's just checking for support. https://browserleaks.com/webgl has THREE toggles. Is this info in there (sorry for being a lazy bum but not into learning about webgl right now, lol) or it something we need to add

e.g.

let types = ["webgl", "webgl2", "experimental-webgl"]
types.forEach(function(type){
	try {
		let canvas = window.document.createElement("canvas")
		try {
			var context = canvas.getContext(type)
			if (!context) {
				throw new Error()
			}
			console.log(type, "supported")
		}	catch(e) {
			console.log(type, "not supported")
		}
	} catch(e) {
		console.log("canvas failed")
	}
})

I think I can just take the objects and pull things out - I need to check various things as RFP protected, so they need to be separate.

Since we are not appending the element to the page,

doh. nothing to remove. nice. BTW not creating the canvas is a tiny perf win :) woo!

@Thorin-Oakenpants
Copy link
Contributor Author

#234 (comment)

can you add that to your gist so I can copy it, thanks

@abrahamjuliot
Copy link
Collaborator

Refactored: https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2

  • added the option to check experimental-webgl separately
  • added categories in final output
  • added experimental concept gpuHash. The length may vary, so we can feed this to a SHA or mini hash instead of join
  • dropped the supported functions since it is just browser feature detection
  • added the major performance caveat detection
  • added Direct3D detection

@Thorin-Oakenpants
Copy link
Contributor Author

awesome, thx

@Thorin-Oakenpants
Copy link
Contributor Author

the gpuBrand is redundant (but nice) - I can drop that

the gpuHash doesn't make much sense to me - sorting values could/would create collisions ? for example on it's own

  • me: webgl - NVIDIA:0:1:8:16:23:24:30:32:127:1024:4095:16384:32767
  • what do the numbers correspond to (we already have that data in parameters?)
    • e.g. VERTEX_SHADER_BEST_FLOAT_PRECISION: Array(3) [ 23, 127, 127 ]
  • why a Set and only one of each number
    • see above I have two 127's

This doesn't make a good fingerprint for max entropy, but I get it, it's a nice wee string. Unless I'm missing something

@Thorin-Oakenpants
Copy link
Contributor Author

Thorin-Oakenpants commented Apr 6, 2023

gpu doesn't handle an empty string, returns ,

	let gpuV = cleanFn(parameters.UNMASKED_VENDOR_WEBGL),
		gpuR = cleanFn(parameters.UNMASKED_RENDERER_WEBGL)
	const gpu = String([gpuV, gpuR])

edit: well, it does handle it, it's just not very reader friendly :)

@Thorin-Oakenpants
Copy link
Contributor Author

I'm confused. Top is TB (which has RFP webgl mitigations - you get the same result in FF with RFP enabled), bottom is nightly with no RFP.

The bottom test looks fine, but at first glance seem to have duplication, but the top test - why are we not returning a debugRendererInfo object with undefined items (and extensions that mimic this RFP behavior). Why is webglcontext.renderer different to gpuRenderer ?

https://bugzilla.mozilla.org/show_bug.cgi?id=1337157 - lemme look at this. I know it's supposed to return undefined or blanks and/or Mozilla

i-am-a-bit-confused

	const Categories = {
		'debugRendererInfo': [
			'UNMASKED_VENDOR_WEBGL',
			'UNMASKED_RENDERER_WEBGL',
		], // snip
	}

	parameters = { // snip
		UNMASKED_VENDOR_WEBGL: getUnmasked(context, 'UNMASKED_VENDOR_WEBGL'),
		UNMASKED_RENDERER_WEBGL: getUnmasked(context, 'UNMASKED_RENDERER_WEBGL')
	}
	parameters.DIRECT_3D = /Direct3D|D3D(\d+)/.test(parameters.UNMASKED_RENDERER_WEBGL)

	gpuVendor = parameters.UNMASKED_VENDOR_WEBGL
	gpuRenderer = parameters.UNMASKED_RENDERER_WEBGL

@Thorin-Oakenpants
Copy link
Contributor Author

so gpu (which I split into gpuVendor and gpuRenderer) is redundant then? right? It's a direct lookup of debuginfo? right?

@abrahamjuliot
Copy link
Collaborator

Yeah, gpu is non-essential. I forgot. GPU showing up under render is this issue. It can be ignored, but the way to around is to feature detect the version and then use renderer instead of debug info, but its only necessary to remove the console warning.

mdn/content#12689

@abrahamjuliot
Copy link
Collaborator

abrahamjuliot commented Apr 6, 2023

gpuHash

This too is not needed.

It can be useful to put known good fingerprints into a lookup table (similar to known good audio sums). Clashing is likely and okay, as long random WebGL fingerprints find it hard to fit in. I'm guessing, based on limited data on CreepJS, the table size would require no more than 100 hashes. Everything unknown can be questioned until it is established trustworthy. This is more of a test concept, I suppose. It would require many samples.

@Thorin-Oakenpants
Copy link
Contributor Author

tis all cool. The RFP notation should be pretty simple now I refreshed my memory as to what we were doing with it - there's more to it than just vendor/renderer. But easy to check the few places it shows.

but its only necessary to remove the console warning

I log all errors as part of the error entropy :)

gpu/gpuHash/gpuBrand

no worries. I get it was experimental - but this is gecko and all I care about is Tor Browser and RFP. If I check prototype lies (I'll need to check) then I can just return the whole thing as useless (might revisit later)

when we collect data from surveys (via a Tor Browser annual or bi-annual FP drive - 100% opt-in with one test per profile), we can just reject collecting tainted data based on prototype lies alone - i.e either it is all empty or it matches NoScripts signature

@abrahamjuliot
Copy link
Collaborator

There's also these anti-detection browsers that fake the GPUs at engine level. No prototype lies. For Firefox, I think they use what is called "Stealthfox Browser". The only way I'm aware of to detect them is by using a look-up table of known good hashes. WebGL is too high entropy for a local lookup, but this lower entropy hash works. I have not used it on CreepJS, but I have the data visually from last 60 days of bad traffic.

@Thorin-Oakenpants
Copy link
Contributor Author

gummy bear browsers :) lols ... I'm not too worried about it TBH, I need to focus on actual TB and FF + RFP users

@abrahamjuliot
Copy link
Collaborator

abrahamjuliot commented Apr 6, 2023

Something I discovered, many of them have a frozen max stack size fingerprint. Something with the way it's compiled, I guess.

@Thorin-Oakenpants
Copy link
Contributor Author

Thorin-Oakenpants commented Apr 6, 2023

I'm not entirely sure what you mean by that, tell me more :) - you had me at "something"

Yeah, math PoCs are great, like known pixel tests, joining chars vs individual chars in textmetrics width, domrect etc. Enumerating goodness is hard and no bulletproof - so I understand the mini simplified hash - like the simplified (less measurements) of https://arkenfox.github.io/TZP/tests/domrectspoofratio.html that covers multiple chrome results

I just had to drop enumerating goodness for audio in FF (TZP 2.0 is gecko only) - math library changes on android, and they will change again, and then RFP is also doing something with it (or it will apply for all) - https://bugzilla.mozilla.org/show_bug.cgi?id=1358149#c26

@Thorin-Oakenpants
Copy link
Contributor Author

you had me at "something"

do you mean recursion, stack depth, rather than some webgl thing?

@abrahamjuliot
Copy link
Collaborator

Yeah, recursion, stack depth. I have not tested for Firefox, but it's somewhat of an unbeatable fingerprint for custom Chrome builds that are slow to update.

@Thorin-Oakenpants
Copy link
Contributor Author

OT: but I had an email exchange with a moz dev about this, and it's a good (fuzzy) FP for determining ion and jit etc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

2 participants