Shapes and Colours Considered Insecure
https://franca.lukegrahamlandry.ca
I enjoy writing programs that draw pretty pictures on the screen.
In an ideal world those programs could just be an executable that you
run and then displays the pretty picture.
Unfortunately, if I want to show someone my pretty pictures,
it's a lot of friction to ask them to download an evil native executable.
Much easier instead to ask them to click on a link
that opens a blazingly sandboxed website.
Sadly that too is easier said than done.
The programs I've been writing tend to emphasize (the computation
and the production of the pretty picture) over (having an interface
for doing a task or spending lots time doing nothing as fast as possible
while waiting for another computer, as websites are wont do to).
I also like to tell myself that my programs are not slow which means
I want (threads so I can do lots of things at once), (fragment shaders
so i can do even more things at once), and (to use a language
that can express memory layouts that are efficient for computers to operate on).
The lovely developers of browser technology have answers to my desires
in the form of webworkers, webgpu, and webassembly.
In the name of story telling let's imagine that I've created my
program using all these modern, developer friendly tools.
The last step is to preview it on my computer
to make sure the pictures look right before I release it to the internet at large.
One does not simply "run the program"
For a number of years I've been familiar with the observation that the security of websites
is a decreasing function of the density of pretty pictures they produce.
I have healthy distrust of wenebsnite as a platform
and a corresponding lack of confidence that I'm understanding it correctly,
so if you happen to read this and know that I'm wrong, please tell me.
can i has file? (no)
For trivial website like the one you're reading now,
I can just open the file directly in a browser with a file:/// url.
But it seems that breaks down when you want to reference other (.js, .wasm) files in the directory.
For example, adding a script tag with type="module" yields the following:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at file:///[...]. (Reason: CORS request not http).
Module source URI is not allowed in this document: “file:///[...]”. index.html:86:45
As a foreshadowing of things to come, typing SharedArrayBuffer in the console will happily report that it's undefined.
U+201C story time
Such joyous memories triggered by pasting the curly quote from that error message.
In university I had an assignment that involved writing a kernel module
that would register a character device that would return back whatever the last byte written to it was.
The command they typed in a pdf to test it was something like
echo -n “a” > /dev/foo
which when pasted into bash may disappoint the uninitiated who later read 0x9d instead of 0x61.
can i has server? (no)
python -m http.server 8000
Now I can go to http://localhost:8000 to see an exciting change to the errors in the console.
The WebAssembly.Memory object cannot be serialized. The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy HTTP headers can be used to enable this.
A quick consultation with https://developer.mozilla.org yields conformation.
WebAssembly.Memory: "The WebAssembly.Memory object is a resizable ArrayBuffer or SharedArrayBuffer"
SharedArrayBuffer: "To use shared memory your document must be in a secure context and cross-origin isolated."
WebGPU API: "Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers."
The web people are having a MELTDOWN over spooky ghosts and SPECTREs (the joke is funny because...).
Which is fair, hard to argue with that, but means more work for me.
why meltdown over counting up?
The big threat they're protecting me from is that if you can spawn a thread
that spams atomic increments on a shared memory address,
another thread can use that as a high precision timer.
And like ok so what but with a timer you can observe whether a memory access is a cache hit
and with speculative execution your cpu allows you to put specific memory locations in cache based
on a value stored in memory you shouldn't be able to read.
Then all of a sudden you can turn arbitrary code execution into arbitrary memory reads.
Which sounds a bit dumb when you say it like that but websites were supposed to be blazingly sandboxed
so it's a big deal to be able to leak something through that.
You can ask dear wikipedia
for a less flippant explanation, it's very cool if you're in to that sort of thing.
can i has security? (yes)
why no --tls-(cert, key)?
I happen to have python 3.13.7, but in 3.14 they added --tls-cert and --tls-key parameters to the command above
which would let me get father without writing a program, but would still need the openssl
incantation to generate the keys and wouldn't fix the headers problem so i'd still need the program anyway
to make it actually work.
Luckily it's easy to use http.server as a library instead of a script and wrap it in an SSLContext
(I stole this code the old fashioned way).
Then it's only a few extra lines to poke in the magic Cross-Origin-foo headers.
# openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout target/key.pem -out target/cert.pem
HOST = "localhost"
PORT = 8000
import http.server
import ssl
import os
class Handler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
super().end_headers()
os.chdir("target/web")
with http.server.HTTPServer((HOST, PORT), Handler) as s:
print("https://" + HOST + ":" + str(PORT))
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(keyfile="../key.pem", certfile="../cert.pem")
s.socket = ctx.wrap_socket(s.socket, server_side=True)
s.serve_forever()
Using a self signed certificate like that means there's a scary warning the first time I open it in a browser but I'm well past the point of caring about that.
This attempt does work, but it makes me sad...
can i has smaller dependencies? (yes)
I have perhaps an unhealthy obsession with my programs being self contained.
Let's just gloss over the fact that this whole endeavour means depending on a bazillion lines of chrome.
I don't like the aesthetics of hoping the openssl executable is globally installed somewhere convenient.
If I wanted to commit to python, I could presumably use it to generate the certificate instead but
running (pip install cryptography) is kinda the opposite direction of where I want to go.
As a more concrete motivation I don't want to add openssl or python to my tests/flake.nix
but I do want to be able to run (nix develop ./tests) and see my website somehow.
The
first step is replacing openssl with mbedtls,
which from past experiments I know can be compiled by
my c compiler.
The command before used the openssl binary for two things: generating an rsa key and generating a tls certificate.
One of the mbedtls example programs
cert_write.c
shows how to do the latter and the former is very similar to an example from their
getting started docs with different magic constants and a bit of extra fiddling around to use mbedtls_pk_write_key_pem.
So with that I can produce key.pem and cert.pem files that work with the python program from before.
As a matter of principle I'd also like to replace the python program with one written in
my language.
Mbedtls also has an example of a minimal server that just returns a hard coded result for every request:
ssl_server.c.
It turns out that once someone else handles the cryptography/handshake/whatever part for me, the http part is trivial.
The gist is use the socket,bind,listen syscalls to get a file descriptor for a new socket,
then the accept syscall gives me a new file descriptor when someone makes a request.
After calling mbedtls_ssl_handshake, accessing that last fd goes through
mbedtls_ssl_read/mbedtls_ssl_write instead of making the read/write syscalls directly.
The requests coming in look something like:
GET /[path] HTTP/1.1\r\n
Host: [localhost:8000]\r\n
[more header lines]\r\n
\r\n
Then response I send back is something like:
HTTP/1.0 200 OK\r\n
Content-Type: [mime]\r\n
[more header lines]\r\n
\r\n
[data]
with the [data] from reading the [path] on disk.
The Content-Type header is important for application/wasm and application/javascript or the browser won't run the code.
The headers (Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy) are also needed as discussed before.
I also send Content-Length because I use it for a progress indicator in the ui the first time the wasm is loading.
In the end I have a new command that generates the key/cert if missing and then runs the https server like before:
franca examples/web/serve.fr port 8000 -folder target/web
I acknowledge that mine is probably slower than the python one.
Unlike the programs that bring me joy, this one spends all of its time doing nothing as fast as possible.
Note again that this is all to use the web browser on my own computer to get the program that I wrote that lives on my own computer to run so I can test it.
I care zero for security, it's just my computer talking to itself.
If there's some terrible vulnerability in the version of mbedtls I pinned, that's fine, it's just my computer talking to itself.
If I did a bug because I wrote it in a evil unsafe language, that's fine, it's just my computer talking to itself.
Your millage may vary. This is not medical advice.
For the time being I'll leave it here and keep using mbedtls rather than trying to roll my own of that too...
rome wasn't built in a day as they say.
can i has deploy? (yes)
Now that I can test my program, the easiest way to give it to the internet
is to just commit some yaml junk and have github pages do it.
Unfortunately they don't set the http headers to make it cross-origin isolated so it doesdendt' work :(.
Putting it behind cloudflare lets me have an "HTTP Response Header Transform Rule" that adds them.
This is good because it means that my website can die if either microslop OR clownflare die instead of just depending on one.
But in all fairness I'm too lazy to do it myself so I'm not exactly at peak moral high ground for complaining.
I also tried srht.site which is much less painful but it also doesn't set the magic header
and cares not for cloudflare, which I agree with in principle but means I can't cheat and set them myself.
The website I've been trying to make happen this whole time
is a playground for my language that jit compiles my various toy programs to wasm.
There's a link to the live version at the top of this page if you want to try it.
It's a bit of a gamble which browsers it works in;
may the force be with you and with your spirit.
was this all a waste of time? (no)
Since Firefox 153 (July 21, 2026), there's Local Mode.
It seems to be a painless way to run my own program on my own computer
(even if they do dangerous things like draw pretty pictures and be not slow).
This is an amazing innovation. Truly a marvel of modern engineering.
So I guess the moral of the story is just never even bother doing anything.
Just hide in your basement and hope someone else decides to fix your problem
out of the goodness of their heart.
That moral makes me sad to I have to tell myself my way is still cool because...
- i distrust documentation that shows you screenshots of how to update your settings. it seems many organisations incentivise auto-updates that change ui at random without pesky details like updating old documentation.
- "These mappings are only functional when DevTools are opened." does that mean i have to keep them open? why must we steal my screen real estate just to change how urls work?
- unclear if chrome/safari have an equivalent so maybe it still matters for testing elsewhere.
- my first commit on the wasm backend for my compiler was on Dec 27, 2024 (8bc51ec) so it would have been quite a long wait for them to come to their senses.
But hey my prayers have been answered!
It would be immature of me to just complain about them releasing features that make my life easier.
There's even a button for it in the dev tools to automatically add it to settings so my first complaint is unfair to them.
Without further ado, let's see what we've all been waiting for:
The WebAssembly.Memory object cannot be serialized. The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy HTTP headers can be used to enable this.
Did you really believe computers work? NOPE.
now we must actually write the code
It turns out that even once I can run a program, porting all of my programs
to run in a website is more of a hassle than supporting arm+amd+riscv or macos+linux.
Still, I find this part less frustrating than the https struggles above to get it to run at all.
If porting to a new platform requires work to adapt to the primitives they want to provide
that's fine by me as long as they can produce documentation about what the rules are.
I'm trying to frame this section in a way that sounds less like complaining
and more like a useful overview of what it takes to put your fun new programming language in a website.
TODO: explain these in a useful way
- jit by making new modules
- function pointers are table indices
- no shared tables so jit between threads is slow
- strict signatures so can't do tricks with saving all registers for unjitted function
- can't instantiate big module on the main thread
- can't talk to events off the main thread
- wgpu canvas context can be sent to other thread which is nice (but only one still :()
- wgpu objects can't be stored in linear memory
- futex is as normal
- no virtual memory is kinda sad, can't lazily zero init things
can't do free safety checks by unmapping memory.
- no setjmp/longjmp, there are exceptions (that i haven't used yet) but can't do coroutines that way. not a big deal in my language but there are a few fun examples that won't work without them.
- threads are workers but node has a different api just to waste your time
whose job is it to be the platform?
this section departs from fun recount of my toy projects
into me just being young man yelling at cloud.
my hope, as always, is that i will be wrong on the internet
and incite someone into teaching me something.
I think wasm has a bit of an unachievable goal.
If you can sift through the marketing speak, I think a major purpose of producing a new environment
and making everyone rewrite a bunch of code is to let different programming languages
interoperate more effectively.
The strategy for getting there is pulling the implementations of
various runtime features up into the platform
instead of leaving them to individual programming languages to provide.
On real computers, every cpu vendor needs to provide a small set of primitives
and every programming language vendor needs to know how to lower exactly the high level
features they want on to those primitives.
On a website, every browser vendor needs to know how to lower every high level
feature a programming language might want on to the primitives of a real computer,
and every programming language has to hope that the high level features
they want happen to be blessed by the browser people.
This difference is most obvious in languages that try to have features that are nontrivial.
Real computers give you the power to observe and change the state of registers/stack/etc.
so you can do whatever wacky control flow, memory layout, etc. you want.
Nobody had to beg Intel to let them write a garbage collector, you can just... do that.
On the other hand, the wasm people had to spend years thinking about how to add a gc extension.
The result is something that you can only use if your object model maps trivially on to javascript's.
If your language needs (to share an object between threads) or (internal pointers) or (managed/unmanaged memory to exist in the same address space) or (deterministic finalization timing), etc., you're shit out of luck.
So in the MVP, the GC-ed language complained that their binaries were big.
Now they have a builtin solution... unless you want control over its semantics... which everybody does... but whatever.
This situation has to play out again for every language feature.
There are two proposals for exception handling implemented in the major browsers.
Does every wasm runtime keep supporting both (legacy and exnref) until the end of time or do they break programs?
The scheme people want delimited continuations, should they wait for the stack switching proposal to be implemented (and hope it matches the semantics they need)?
If wasm ends up with first class support for call/cc
(which it won't)
does that supersede all the other proposals for increasingly complex control flow?
Is the web platform ever allowed to break websites to take redundant features out?
I don't understand how this approach can lead anywhere other
than an infinite amount of work spread over an infinite amount of time
that culminates in getting back to where you were originally but slower.
In many ways the wasm instruction set is much more sane to implement
(either output from compiler or execute in an emulator) than any of the native instruction sets.
I argue the current improvement isn't based on a superior design philosophy that learns from the past,
but rather just that there's been a lot of time since 1978 to pile vector extensions on top of an 8086
and much less time since 2017 to add inane features to wasm.
You can always just give up and do everything yourself in linear memory
(and hope there are good enough jits to un-stupid it into the code you wanted to emit in the beginning).
But then it is not the interop that makes the wasm useful but something else entirely.
Another note is that I've been conflating wasm (the instruction set) and the web embedding of it.
There are technically other people that use it, but like Web is in the acronym... there's a pretty clear favourite use-case.
prev: Prospero Challenge Entry
next: Risky Emulation
all
change log
[Apr 27, 2026] wrote the first section
[Aug 4, 2026] "Local Mode", platform feature creep.