Notes10 min · 23 Oct 2026

What actually runs my code?

A bug in PHP let attackers into sites whose own code had no flaws. Making sense of that takes three layers, engine, harness and environment, and they explain far more than PHP.

  1. 01Where it started
  2. 02Three layers
  3. 03What the engine does: translation
  4. 04Underneath: processes and threads
  5. 05Two ways to serve a request
  6. 06Where it runs
  7. 07Where state lives when machines forget
  8. 08Back to PHP2Shell

01Where it started

Two things arrived at once. A video about how PHP serves a web page, and the news of PHP2Shell: two bugs in PHP itself, patched on 21 October 2026, that let an attacker run their own code on a server. The odd part was who was exposed. A site could be written perfectly and still be taken over, because the flaw wasn't in the site. It was in the program that runs the site.

Fig 01
Attacker's code runs
  1. Your code

    WordPress, Laravel, your own script. Written without a flaw.

  2. PHP engineThe hole

    Reads the upload's progress into the session before your script starts.

  3. Web server

    Apache or Nginx takes the request and hands it to PHP.

Request with a crafted upload
The request never reaches the code you wrote. PHP handles upload progress first, and that is where the bugs are.

That raised the real question. Everyone says Node and PHP are "runtimes", and agents are often split into a model, a runtime and an environment. So what is a runtime, and why can it be the weak point?

02Three layers

A language on its own is a set of rules for writing. To run, code needs three things.

  • An , which turns the code into work the processor can do. It can calculate, but it can't touch anything outside itself. V8, the engine inside Chrome and Node, can add numbers all day and can't read a file.
  • A : what the engine is allowed to reach, and the loop that decides when code runs. Files, network, timers, the page on screen.
  • An , the place it all lives. A laptop, a browser tab, a container, a function on Vercel.

A is usually the first two shipped together. Node is V8 plus a harness of files, network, processes and an . The browser is V8 too, with a different harness: the page, fetch and storage, and no general access to your disk.

Fig 02
EnvironmentYour laptop, a server, a container
HarnessEvent loop, for as long as the program runs
  • Files
  • Network
  • Processes
  • Timers
  • Page
  • Storage
Engine
V8
Pick a runtime. The engine barely changes. What it can reach changes completely.

Same engine, different tool belt. Node and Chrome run the same V8; what they can do is decided by the harness round it.

The chat first lined up runtime with runtime. The sharper match came a message later: an agent's harness is the runtime's bundled tools, plus its loop. A model is an engine. It can reason and can't run a command. The harness gives it tools and runs the cycle of think, call a tool, read the result, go again. That is why one model behaves so differently depending on what it is handed.

Engines vary less than you'd think. Deno is V8 with a different harness. Bun uses JavaScriptCore, Safari's engine. Cloudflare Workers are V8 with network but no file system, on Cloudflare's machines. Anything new can be asked the same three questions: which engine, which harness, which environment?

03What the engine does: translation

A processor only runs . Whatever you write, something has to translate it, and languages differ mostly in when. "Compiled" and "interpreted" are two answers to that one question.

Fig 03
Before it ships · your machine
Source
main.go
Compiler
go build
Binary
machine code
While it runs · their machine
CPU
runs it as is
Translates
Where translation happens, from your machine before shipping to the user's machine while it runs.

C, C++, Go and Rust are translated by a before they ship. The result is a binary for one kind of processor and system, and nothing translates while it runs. Java and C# stop halfway: the compiler makes , which no processor runs, and the JVM or .NET runs it. That halfway step is what "write once, run anywhere" meant. The bytecode is the same everywhere; only the JVM is built for Windows, Mac or Linux.

JavaScript, PHP and Python ship as source, and the engine translates on whatever machine runs them. Modern engines then cheat: they watch which code runs often and compile it to machine code on the spot (). That is most of why JavaScript got so much faster. So the line is a spectrum of when, not two boxes.

Go and Rust have no engine in the middle, but they still carry runtime pieces inside the binary. Go bundles a garbage collector and a scheduler for its goroutines. Rust has almost none, and if you want an event loop you pick one as a library, usually Tokio. Either way you copy one file to a server and run it. A Node or PHP app needs Node or PHP installed first.

Why bother interpreting at all? Three reasons, each about people rather than machines. Portability: a website has to run on devices that didn't exist when it was built, so it ships source and each browser brings its own engine. Safety: code that goes through an engine can only do what its harness allows, which is why a web page can't read your files; ActiveX let pages run native code in the 1990s and was a security disaster. And speed of trying: edit a PHP file, refresh, see it.

The costs explain the modern fixes. Interpreting is slower, hence JIT. It needs a runtime installed, hence the appeal of Go's single file. Mistakes only show when the code runs, hence TypeScript, which puts a compiler-style check back on top of JavaScript.

Why no Python in the browser? Browsers agreed on one language in the 1990s, and every attempt at a second (VBScript in Internet Explorer, Dart in an experimental Chrome) died because nobody writes for one browser. The side door is : Pyodide compiles Python's own engine into it, so a Python engine runs inside the browser's engine. It works, after a download of several megabytes, and it still reaches the page through JavaScript, because the browser's harness only hands the page to JavaScript.

04Underneath: processes and threads

Every runtime asks the operating system for the same two things. A is a running program with memory the system keeps private to it. A is a line of work inside a process; threads share their process's memory.

Think of kitchens. A process is a kitchen with its own pantry. Threads are cooks in one kitchen, sharing the pantry. More kitchens is safer and expensive. More cooks is cheap, and they bump into each other.

Fig 04
Process 1
Cook 1
Pantry · memory
Process 2
Cook 2
Pantry · memory
Process 3
Cook 3
Pantry · memory

Like PHP-FPM: each worker its own process. Three pantries to fill, and nothing shared.

Crash one cook. In separate kitchens, one stops. In a shared kitchen, the whole kitchen goes.
  • PHP-FPM's workers are separate processes. That is where PHP's isolation comes from.
  • Node is one process with one main thread running your JavaScript. Underneath, a small pool of helper threads, four by default, does some file and system work. Your code only ever runs on the one.
  • Go's goroutines are lighter than threads. Its runtime juggles thousands of them on a handful of real ones.
  • Chrome keeps sites in separate processes, so one crashing page doesn't take the browser with it.

This is the floor of this chat. The operating system hands out processes and threads; each runtime decides how to use them. Everything in the next section is a different answer to that.

05Two ways to serve a request

Most of a web request is waiting. Waiting for the database, a file, another API. The computing in between is tiny. PHP and Node differ in what they do during the wait.

PHP: a waiter per table. PHP-FPM keeps a . Each request gets a worker, start to finish. When the script asks the database for something, that worker stands at the kitchen window until the answer comes. When the page is sent, the worker is wiped clean and goes back to the pool. If every worker is busy, the next request queues.

Node: one waiter for the room. One thread runs all your JavaScript. When a request needs the database, Node hands the wait to the operating system and moves on to the next table. When the answer comes, a note joins a queue. The is literally a loop asking: is anything in the queue? If so, run the code that was waiting for it.

Fig 05
6 at once
Req 1
Req 2
Req 3
Req 4
Req 5
Req 6
0 ms
100 ms
200 ms
300 ms
Last reply
240 ms
Workers held while waiting
4 of 4
Queued for a worker
2
Ink is computing; dots are waiting. Add requests, then try blocking Node's one thread.

That is what await means in Node: park me here, serve someone else, come back when my data is ready. In PHP the same call simply holds its worker until it's done.

The two came from different worries. PHP began in 1994 as tools for a personal home page, and it is built so you never think about other requests: your script runs top to bottom, shares nothing, and if it crashes it takes down only its own waiter. Ryan Dahl built Node in 2009 because servers were spending their resources waiting, and he bet that if waiting cost nothing, one process could hold thousands of connections.

PHP buys safety and simplicity by spending resources. Node buys efficiency by asking you never to block the loop.

The lifecycles follow. A PHP script starts fresh for every request and everything in memory is thrown away after, which is why /about.php feels like a file that becomes a page. A Node program is the server: you start it once with app.listen(3000), and what's in memory stays between requests. The lines have blurred since. FrankenPHP's worker mode keeps PHP in memory, Node can start extra threads for heavy work, and Go takes a third path, with code that reads top to bottom like PHP's and is scheduled like Node's waiter.

06Where it runs

Containers. Docker starts an ordinary process and fences it off: its own view of the file system, its own network, its own list of processes, limits on memory and CPU. Inside, it looks like a whole machine. Outside, it is one more , sharing the machine's kernel. The fills the fenced-off space with a runtime, your code and its dependencies, which is why containers ended "works on my machine". A fakes a whole computer with its own operating system: a separate building rather than a walled-off kitchen. On a Mac you use both without noticing, because containers need a Linux kernel and Docker Desktop runs a small Linux virtual machine to hold them.

Serverless. You hand the platform a function, "when a request comes, run this", and it owns everything else: machines, runtime, scaling, patching. There are still servers; they just aren't yours to think about. In Next.js a route.ts that exports GET is a , and so is a server component that fetches data.

Fig 06
No copy running

Nothing has asked yet. Nothing is running, and nothing is being paid for.

Send requests. The first after a quiet spell waits for a cold start; the rest find a copy already warm. Lengths are illustrative.
  • No memory between requests. Any copy can vanish, so what must last goes in a database, a cache or storage.
  • Time limits. Functions are for short jobs.
  • You pay for runs, not for idle hours.

Vercel is an environment, not a runtime. Deploying the portfolio puts code in three places. The build runs Node to execute next build. Pages that can be made ahead become plain files on Vercel's CDN, and only the browser runs anything. What stays dynamic runs as functions, on Node by default, or on the Edge runtime, which is V8 with the browser-style harness: fetch, no file system. Same engine, smaller harness, the Node and browser contrast again. Vercel now reuses warm copies for many requests at once (Fluid compute), so the lifecycle sits between PHP's and Node's. Treat memory as disposable anyway.

How a chat like this one runs. The model isn't started up for your chat. It runs on GPU servers shared by huge numbers of conversations, and each message sends the whole conversation in again; it reads, replies, and keeps nothing of you. The harness is the app's loop: it sends your message, and when the model asks for a tool, such as a web search, it runs the tool and feeds the result back. The environment is the chat's own sandbox, a fenced-off Linux workspace where file and shell tools run. The "instance" you imagine for a chat is real, but it is the environment and the transcript. The model is the shared engine, one V8 serving everyone.

07Where state lives when machines forget

Everything above throws memory away. PHP wipes it after each request, serverless copies are frozen or killed at will, and containers are restarted for deploys, scaling and failures. That is fine for a request that takes 200 milliseconds. It is a disaster for an agent run that takes twenty minutes, calls the model forty times, edits files and waits for you to press Approve.

Restate's answer is . Your code wraps every step that touches the outside world in ctx.run. Restate, a separate program your code reports to, writes each step's result to a before your code is allowed past it. If the machine dies, Restate calls the function again from the top on any free machine and sends the journal along. Steps already in the journal return their saved result at once; the first step that isn't is where the code is live again.

Fig 07
Attempt 1

Restate calls the handler on container A.

  1. 1
    const plan = await ctx.run("ask model", () => llm.plan(task));
  2. 2
    await ctx.run("write file", () => sandbox.write(plan));
  3. 3
    await ctx.sleep(60_000);
  4. 4
    await ctx.run("tell user", () => notify(user));
Journal · kept by Restate
  1. Empty.
Run it a step at a time and crash it wherever you like. The rerun fast-forwards through the journal.

Waiting is cheap too. For ctx.sleep, or for an approval, Restate journals "waiting until" and lets the function end. The container is free for someone else. When the time comes, the replay brings the code back to that line.

It is Node's await at the scale of a cluster: park me, free the machine, wake me anywhere when it's ready, even hours later.

It has one rule. The code reruns from the top, so everything outside the wrapped steps must do the same thing every time. Math.random() or the current time, unwrapped, could send the replay down a different path from the one in the journal. Anything unpredictable goes inside ctx.run, so its answer is recorded too.

Replit runs its agent's orchestration on Restate. In the three layers: the model is called over an API, shared and stateless. The harness is the agent loop, in disposable containers. The environment is the sandbox where the agent's code runs, walled off harder because the code is untrusted. Restate is the memory under the harness, so the session lasts even though the machines don't.

08Back to PHP2Shell

Now the bug reads plainly. PHP's engine is written in C, which makes the programmer hand memory back by hand. Its session upload-progress feature, on by default since PHP 5.4, tracks a file while the upload is still arriving, before your script starts. The two bugs leave the engine holding pointers to memory it has already handed back, a , and one of them happens when PHP runs out of memory at the wrong moment. Fill that memory with crafted data and the engine reads the attacker's data as its own.

And the fresh start every request? PHP does keep one thing between requests, on purpose: the session, saved as a file on disk. Upload progress is written into it. The public exploit chain poisons that file, then gets PHP to run it. The wiped memory was never the risk. The one door PHP leaves open between requests was.

The engine had the hole, so it didn't matter how good the code on top was. A model can behave perfectly while its harness lets anyone run commands.

Fixed in PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12. Turning session.upload_progress.enabled off narrows it but doesn't close it; only updating does. Reports of attacks in the wild disagree. A Next.js site isn't touched, but it is the same reason Node and V8 updates matter: your code is only as safe as what runs it.

Connected

  • Hole · same thingSuggested
    What happens when I press play in Stremio?

    stremio-server is a long-running Node program on your Mac, the Node lifecycle exactly: started once, stays up, keeps its 25 GB cache. The window you browse in is a web app with the browser's harness. Same engine family, two harnesses, one app.

  • Hole · same thingSuggested
    Why does thinking harder cost more?

    The model here is the engine there. Thinking stops at the reply; this note picks up the harness round it, the tool loop that hole left out.

  • Hole · same patternSuggested
    Why does thinking harder cost more?

    Turned round. Thinking and Stremio both have one job and different engines (relay, tube, transistor; tracker, DHT). Here it is one engine and different harnesses: V8 in Node, in Chrome, in a Worker.

  • Note · same thingSuggested
    What is actually in Bun?

    That note's Node and Bun are two runtimes in this one's sense, and its last section is the earlier chat on containers this one leaned on: the image as a whole folder tree, packed up.

  • Chat · no note yet
    Restate infrastructure overview

    The earlier chat dissecting Restate and Replit's agent set-up. This note's last section leans on it.

Terms

Checked

What the chat had wrong or only half right, against sources. The note above already says the corrected thing.

  1. Python, PHP and JavaScript are interpreted.All three compile to bytecode or opcodes first and then run that. PHP caches its opcodes and has had a JIT since PHP 8.0. Still a spectrum of when.PHP manual, OPcache
  2. BASIC was interpreted in the 1960s so students could see results at once.Dartmouth BASIC (1964) was compiled; it only felt instant. Interpreted BASIC came with the microcomputers of the 1970s.Dartmouth College, BASIC at 50
  3. Chrome gives each tab its own process.Roughly each site, not each tab: pages from the same site can share a process.Chromium, Site Isolation
  4. Nothing stays between your messages except the transcript.Providers can keep the already-read start of a conversation cached for minutes, so it isn't read again from scratch. It is a cache of work, not a memory of you.Anthropic docs, prompt caching
  5. A container is a process sharing the computer's operating system.True on Linux. On a Mac, Docker Desktop first runs a small Linux virtual machine, and the containers share its kernel.Docker docs, Docker Desktop on Mac
  6. PHP2Shell keeps invalid session state reachable across requests, breaking PHP's fresh start.Memory is still wiped. What lasts is the session file on disk, which is what sessions are for; the public exploit chain poisons that file. Sources also disagree on which CVE is which bug, and on attacks in the wild.Akamai, Understanding PHP2Shell
  7. Restate calls itself a runtime.Couldn't confirm, so it is left out. Restate describes a durable execution engine that replays a persisted log.Restate docs, durable execution