Run docker inspect on any Cloud Native Buildpacks image you have ever built and look at the entrypoint. It is not your app. It is /cnb/lifecycle/launcher, a ~2.9 MB statically linked Go binary that nobody on your team wrote, reviewed, or chose a version of. It is the reason your scanner reports net/http and crypto/tls CVEs against a Ruby service that never opens a socket.
Two open proposals in buildpacks/rfcs are trying to do something about it, from opposite ends. RFC #336 wants to stop welding the launcher into the image your build produces and let the run image supply it instead. RFC #338 wants to throw away the Go implementation and rewrite it in Rust. Neither is merged. Both tell you more about where a git-push platform's supply chain is heading than any release note will.
The numbers, up front
Here is what each RFC actually changes, kept in separate rows on purpose — they solve different problems and conflating them is the easiest mistake to make.
| Today | RFC #336 (launcher in run image) | RFC #338 (Rust launcher) | |
|---|---|---|---|
| Launcher binary | ~2.8-2.9 MB, static Go | unchanged bytes, different location | ~700 KB, static Rust/MUSL (RFC target, not a measurement) |
| Launcher layer content | self-contained executable | a symlink into the run image | self-contained executable |
| Who ships the launcher | the builder's lifecycle, at build time | the run image vendor | either |
| Patching a launcher CVE | rebuild every app image | update one run image, then rebase | still a rebuild, but far less often |
| Go stdlib CVE surface | full Go runtime + stdlib | full Go runtime + stdlib | Rust std + serde, toml, libc, rustix |
| Spec impact | — | Platform API change; new rebaser validation | none — drop-in replacement |
The row that gets misread is the CVE row. RFC #336 does not make the finding go away. The run image is part of the final app image and part of the running container's filesystem, so a scanner pointed at your deployed image still finds a Go binary at /cnb/lifecycle/launcher and still attributes every Go stdlib advisory to it. What #336 changes is who can fix it and how fast. Only #338 — the Rust rewrite — actually shrinks the surface the scanner is reacting to.
The treadmill that started this
Both RFCs trace back to the same operational complaint, and it is measurable. Between January 5 and July 13, 2026, buildpacks/lifecycle cut 15 tagged releases in 189 days — a new one every 13.5 days on average, running from v0.20.20 through v0.21.14. A meaningful share of those are Go toolchain bumps chasing standard-library advisories, the same pattern that produced releases like the Go 1.24.4 bump for CVE-2025-22874.
The launcher is caught in that current for a reason that has nothing to do with its code. Container scanners identify Go binaries by their embedded build metadata, then match the compiled-in Go version against the stdlib advisory database. Reachability does not enter into it. The launcher links the Go runtime, therefore the launcher is "vulnerable" to every net/http and crypto/tls CVE in that runtime — a behavior with a long paper trail in scanner issue trackers (Trivy discussion #9490 is the canonical version of the complaint) and one that Paketo has to explain to users often enough to keep a standing FAQ entry about it.
So a platform operator gets a red dashboard for code that parses TOML files and calls execve. The available responses today are to suppress the finding — which trains everyone to ignore that class of alert — or to rebuild. At any real scale, rebuild is the expensive word.
RFC #336: make the launcher a run-image dependency
The proposal, opened April 15, 2026 by Hemant Goyal, adds an opt-in exporter flag (working name --launcher-in-run-image). When set, the exporter still creates the "Buildpacks Application Launcher" layer in the same position with the same name — OCI structure is deliberately preserved — but the layer carries a symlink to a launcher path in the run image instead of a 2.9 MB executable.
Two pieces of metadata make it safe:
io.buildpacks.lifecycle.metadatagainslauncher.type("binary"or"symlink") andlauncher.target(the absolute run-image path).- Run images that provide a launcher advertise it:
io.buildpacks.run.launcher.path=/usr/local/bin/launcher.
The rebaser picks up a new duty it has never had. Today rebase validates OS/arch compatibility and run-image identity and nothing else. With symlink launchers it must verify the incoming run image actually has a launcher at the advertised path — otherwise a successful rebase produces an image that dies at container start on a dangling symlink.
The blast-radius math is the point. Suppose 200 services on your platform. Today, a launcher CVE means a new lifecycle, a new builder, and a rebuild of all 200 app images — which is not only build minutes but a full redeploy, and, crucially, a bet that all 200 still build. A service last touched fourteen months ago may fail on a yanked transitive dependency, a moved package index, or a base-image drift that has nothing to do with the CVE you were patching. Under #336, you push one run image and rebase: a metadata-and-base-layer operation that takes seconds per image, needs no source code, and cannot fail on a dependency that disappeared from npm.
The review thread produced two competing designs worth knowing about, because one of them is more likely to be what most teams actually get. CNB maintainer Juan Bustamante argued for a --launcher-image flag on rebase instead, using the launcher layer digest that io.buildpacks.lifecycle.metadata already records to swap that one layer in place — no build-artifact change at all. He then noted that this is really a special case of RFC #333, "Rebase Buildpack Contributed Layers", and Paketo maintainer Daniel Mikusa's response was blunt about the split: the #333 path "would be the approach used by probably 99% of the Paketo users."
That leaves #336 aimed at the case #333 cannot serve — platforms that do not store whole app images at all. The proposer's architecture keeps app layers in a filesystem and assembles the container at serving time, so any per-image operation, rebase included, is a non-starter. For them the launcher has to be a run-image dependency or nothing.
The shim they are actually piloting
While the RFC sits in draft, the proposer is testing a workaround that any platform can run today, because the lifecycle has always exposed --launcher for injecting a custom binary at build time:
/cnb/lifecycle/creator --launcher=/workspace/launchercat << 'EOF' > /workspace/launcher
#!/bin/bash
exec -a "$0" /usr/bin/launcher "$@"
EOF
chmod +x /workspace/launcherThe -a "$0" is not stylistic — it is the entire trick. Since Platform API 0.4, the launcher selects the process type from its own invoked name, via /cnb/process/<type> symlinks pointing at the launcher (RFC 0045). A naive exec /usr/bin/launcher "$@" sets argv[0] to /usr/bin/launcher, the real launcher looks for a process type named launcher, finds none, and your container fails to start. Preserving argv[0] keeps process-type dispatch intact. The caveat: this puts a bash dependency in the startup path, which a distroless or static run image will not have.
RFC #338: rewrite it in Rust
Opened June 11, 2026 by Terence Lee, this one is narrower and further along in spirit. The argument is not performance. It is that Rust's standard library does not contain an HTTP server, a TLS stack, or a JSON parser — those live in crates you opt into — so the binary a scanner inspects simply has less to match against. The crates the launcher would need (serde, toml, libc) have carried no recent CVEs; Rust's most notable core advisory in years, CVE-2024-24576, concerned argument escaping for Windows batch files.
The implementation notes are specific enough to evaluate:
- Static MUSL builds for portability, across every architecture the launcher supports today.
rustixfor safe POSIX calls, dropping to rawlibcinsideunsafeonly where the spec demands it — notablylibc::dup2insideCommandExt::pre_execto map theexec.dcommunication pipe onto file descriptor 3 before spawning the child.CommandExt::exec(), which maps toexecveand clears the launcher from the process tree entirely, the same semantics as Go'ssyscall.Exec.- A
LaunchErrorenum mapping internal failures to the exact integer exit codes the CNB API mandates, such asPlatformApiIncompatible. - A separate repo (
buildpacks/launcher-rs), so launcher versioning tracks the stability of the API surface rather than the release cadence of thelifecycledev tooling — which drags in heavy dependencies likemobyand inflates the SBOM.
Migration is deliberately slow: run both implementations in acceptance tests, deprecate the Go binary, ship both simultaneously (a ~700 KB size bump is cheap), make Rust the default with Go as fallback, then remove Go. During dual distribution there are three opt-in doors — a runtime environment variable that makes the Go launcher delegate to the Rust one, a pack flag that sets the entrypoint at export, and a platform-level entrypoint override that is invisible to end users.
What this does to container startup, honestly
The RFCs are frequently summarized as making container startup cheaper. Check that claim rather than repeating it, because the answer is mostly no — and the parts that are real are worth stating precisely.
Registry and node storage: real, but small. Launcher layers are content-addressed, so every image built with the same lifecycle version shares one layer digest. Take a fleet of 200 services with 3 lifecycle versions in flight across 40 nodes. The registry stores 3 launcher layers, not 200: 3 × 2.9 MB ≈ 8.7 MB, dropping to 3 × 0.7 MB ≈ 2.1 MB under #338. Worst case, every node pulls all three: 40 × 8.7 MB ≈ 348 MB fleet-wide, falling to ≈ 84 MB — a one-time saving of about 264 MB across the whole cluster. The naive framing (2.2 MB saved × 200 images ≈ 440 MB in the registry) overstates the registry number by roughly 66×, because it ignores dedup.
Per-container start: negligible. After the first container on a node, the launcher is in the page cache. The delta between execve-ing a 700 KB static binary and a 2.9 MB one is a page-in of at most ~2.2 MB from RAM — well under a millisecond. More importantly, the launcher's actual work at start is reading metadata.toml and the per-layer TOMLs, assembling environment variables from layer env directories, sourcing profile.d scripts, and running exec.d binaries over FD 3 before it calls execve. That cost scales with the number of buildpack layers and exec.d hooks, not with the size of the launcher binary. Shrinking the binary by 76% does not move it.
So if you are hoping these RFCs shave meaningful latency off scale-from-zero, they will not. The wins are elsewhere and they are the ones that actually hurt today: patch latency (#336) and scanner surface (#338).
The hard part nobody has solved yet
The strongest objection in the #336 thread comes from lifecycle maintainer Jesse Brown, and it is not about mechanism. It is about who a run image serves.
A run image does not support just one app. Without the build process placing a launcher layer that is known to be compatible with the app being built... there is no real paved path to making sure produced app images can always successfully run.
The launcher must support the Platform API and Buildpack API versions recorded in a given app image's metadata. When the launcher ships in the app image, the build that produced it guaranteed that match. Move it into the run image and the guarantee evaporates: one run image now has to satisfy every app that will ever be rebased onto it, including a two-year-old image whose Platform API the current launcher may have dropped. Someone rebasing weekly to pick up OS patches could find a working app stops launching, with no build and no code change to blame.
The proposed mitigations are real but unfinished: run images advertise supported ranges via labels like io.buildpacks.run-image.launcher.platform-apis = "0.7,0.8,...,0.15", and the rebaser validates the app's recorded APIs against them before swapping. The proposer's own answer goes further — a "meta launcher," a small Go multiplexer bundling several launcher versions that dispatches on the API versions found in the app's metadata.
This is where the operator/community split matters, and it is the honest bottom line for anyone reading these threads as roadmap. If you build the images and run the images and own the run image, you control all three variables and the risk is manageable. If you publish a builder other people consume, you have signed up for a compatibility contract with strangers that nobody has fully specified yet.
What to do on a platform you run today
- Look before you argue. Run
diveordocker inspecton one of your app images and confirm what sits in the launcher layer and what your entrypoint actually is. Most teams have never checked. - Triage launcher CVEs honestly. A Go stdlib advisory matched against the launcher is almost always unreachable — but write that down as a documented exception with a review date, rather than a blanket suppression that also hides the day it is reachable.
- Know your rebuild cost before you need it. The number that matters is not build minutes, it is: how many of your oldest services would still build from source, unattended, right now? That number is what #336 is really about.
- If you invoke the lifecycle directly, the shim is available today.
--launcherhas always been there. Just keepexec -a "$0". - Read the RFC repo, not the release notes. #336 and #338 have been open for months, and the review threads already contain the design that will ship — including #333, which will probably serve more users than #336 does.
The launcher is a rounding error in your image and a recurring line item in your security review, which is exactly the profile of a component that gets ignored until it cannot be. Two RFCs, from a platform operator and a maintainer who disagreed in the same thread, are the first serious attempt to fix both halves. Whichever merges, the direction is set: the thing that execs your app should be patchable without rebuilding your app.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with the build pipeline and the images under your control rather than a vendor's. Star the repo on GitHub or deploy your first app today.
Sources
- buildpacks/rfcs PR #336 — Launcher in Run Image
- buildpacks/rfcs PR #338 — Rust Launcher Implementation
- buildpacks/rfcs PR #333 — Rebase Buildpack Contributed Layers
- RFC 0045 — Launcher Arguments
- buildpacks/lifecycle releases
- Trivy discussion #9490 — Go binaries flagged for unused stdlib CVEs
- Paketo Buildpacks discussion #401 — CVEs reported against builders and app images
- CVE-2024-24576 — Rust
std::process::Commandargument escaping - What is the lifecycle? — Cloud Native Buildpacks docs



