diff --git a/packages/bun-uws/src/App.h b/packages/bun-uws/src/App.h index e326e63a6368..8b3181eb1d99 100644 --- a/packages/bun-uws/src/App.h +++ b/packages/bun-uws/src/App.h @@ -418,19 +418,16 @@ struct TemplatedApp { /* Terminate on misleading idleTimeout values */ if (behavior.idleTimeout && behavior.idleTimeout < 8) { - std::cerr << "Error: idleTimeout must be either 0 or greater than 8!" << std::endl; std::terminate(); } /* Maximum idleTimeout is 16 minutes */ if (behavior.idleTimeout > 240 * 4) { - std::cerr << "Error: idleTimeout must not be greater than 960 seconds!" << std::endl; std::terminate(); } /* Maximum maxLifetime is 4 hours */ if (behavior.maxLifetime > 240) { - std::cerr << "Error: maxLifetime must not be greater than 240 minutes!" << std::endl; std::terminate(); } diff --git a/packages/bun-uws/src/AsyncSocket.h b/packages/bun-uws/src/AsyncSocket.h index 5e2fafae7eb6..e608eedf98d0 100644 --- a/packages/bun-uws/src/AsyncSocket.h +++ b/packages/bun-uws/src/AsyncSocket.h @@ -27,7 +27,6 @@ * to signal error with -1 (which is how the entire UNIX syscalling is built). */ #include -#include #include "libusockets.h" #include "bun-usockets/src/internal/internal.h" diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 67d209e5e54f..6e3bd96fe15d 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -30,7 +30,6 @@ #include #include #include -#include #include "MoveOnlyFunction.h" #include "HttpParser.h" #include @@ -465,8 +464,6 @@ struct HttpContext { /* Returning from a request handler without responding or attaching an onAborted handler is ill-use */ if (!((HttpResponse *) s)->hasResponded() && !httpResponseData->onAborted && !httpResponseData->socketData) { - /* Throw exception here? */ - std::cerr << "Error: Returning from a request handler without responding or attaching an abort handler is forbidden!" << std::endl; std::terminate(); } diff --git a/packages/bun-uws/src/HttpRouter.h b/packages/bun-uws/src/HttpRouter.h index 2783237a40f1..8a48cd6b8ea7 100644 --- a/packages/bun-uws/src/HttpRouter.h +++ b/packages/bun-uws/src/HttpRouter.h @@ -27,7 +27,6 @@ #include #include #include -#include #include "MoveOnlyFunction.h" diff --git a/packages/bun-uws/src/Loop.h b/packages/bun-uws/src/Loop.h index c33f271f626b..d8d8003c74b5 100644 --- a/packages/bun-uws/src/Loop.h +++ b/packages/bun-uws/src/Loop.h @@ -23,7 +23,6 @@ #include "LoopData.h" #include -#include #include "AsyncSocket.h" extern "C" int bun_is_exiting(); diff --git a/packages/bun-uws/src/TopicTree.h b/packages/bun-uws/src/TopicTree.h index 0df624b04d18..ec3a61d1671a 100644 --- a/packages/bun-uws/src/TopicTree.h +++ b/packages/bun-uws/src/TopicTree.h @@ -18,7 +18,6 @@ #pragma once #include #include -#include #include #include #include @@ -106,9 +105,7 @@ struct TopicTree { std::vector outgoingMessages; void checkIteratingSubscriber(Subscriber *s) { - /* Notify user that they are doing something wrong here */ if (iteratingSubscriber == s) { - std::cerr << "Error: WebSocket must not subscribe or unsubscribe to topics while iterating its topics!" << std::endl; std::terminate(); } } @@ -163,7 +160,6 @@ struct TopicTree { /* Subscribe fails if we already are subscribed */ Topic *subscribe(Subscriber *s, std::string_view topic) { - /* Notify user that they are doing something wrong here */ checkIteratingSubscriber(s); /* Lookup or create new topic */ @@ -187,7 +183,6 @@ struct TopicTree { /* Returns ok, last, newCount */ std::tuple unsubscribe(Subscriber *s, std::string_view topic) { - /* Notify user that they are doing something wrong here */ checkIteratingSubscriber(s); /* Lookup topic */ diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts index 2ea21c66e91d..c31aaa2de3c8 100644 --- a/scripts/build/flags.ts +++ b/scripts/build/flags.ts @@ -1512,6 +1512,12 @@ export const stripFlags: Flag[] = [ export function bunIncludes(cfg: Config): string[] { const { cwd, codegenDir, vendorDir } = cfg; const includes: string[] = [ + // Release builds shadow with a #error shim. A single + // include anywhere (our headers, bun-uws, or a WebKit header pulled into a + // Bun TU) drags libstdc++'s globals_io.o into the link and runs + // std::ios_base::Init + the full locale facet set before main. Debug builds + // keep the real header available for ad-hoc printf-debugging. + ...(cfg.release ? [join(cwd, "src/banned-includes")] : []), join(cwd, "packages"), join(cwd, "packages/bun-usockets"), join(cwd, "packages/bun-usockets/src"), diff --git a/src/banned-includes/iostream b/src/banned-includes/iostream new file mode 100644 index 000000000000..dfd898b0077f --- /dev/null +++ b/src/banned-includes/iostream @@ -0,0 +1,27 @@ +// This directory is placed first on the -I search path for Bun's C++ compile +// (release profile), so `#include ` resolves here instead of the +// toolchain header. +// +// is unlike ///: on libstdc++ +// it emits a reference to std::ios_base_library_init (or, on configurations +// without the init-priority attribute, a static `std::ios_base::Init __ioinit` +// object) in every translation unit that includes it. One such reference +// anywhere in the link pulls libstdc++'s globals_io.o in, whose +// _GLOBAL__sub_I.00090_globals_io.cc static initializer constructs +// cin/cout/cerr/clog and their wchar_t siblings before main. That in turn +// references the full std::locale facet set (ctype / numpunct / moneypunct / +// timepunct / messages, for both char and wchar_t), so roughly fifty libstdc++ +// functions run on every Bun process start. +// +// Bun never touches C++ iostreams at runtime. Use fputs/fprintf for error +// output, or WTF's dataLog()/PrintStream in JSC-adjacent code. +// +// Because Bun's own headers include wtf/SIMDUTF.h (and others) from the +// WebKit prebuilt, this shim also catches a WebKit header that starts +// including : it will fail Bun's release build rather than silently +// regressing startup. +#ifndef BUN_ALLOW_IOSTREAM +#error " is banned in Bun release builds: it drags std::ios_base::Init and the full std::locale facet set into pre-main startup. Use fputs/fprintf for stderr output. See src/banned-includes/iostream." +#else +#include_next +#endif diff --git a/test/internal/source-lints/no-iostream-include.test.ts b/test/internal/source-lints/no-iostream-include.test.ts new file mode 100644 index 000000000000..2ef0fa212c33 --- /dev/null +++ b/test/internal/source-lints/no-iostream-include.test.ts @@ -0,0 +1,51 @@ +import { Glob } from "bun"; +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import path from "node:path"; + +// is unique among the C++ stream headers: on libstdc++ it emits a +// reference to std::ios_base_library_init in every TU that includes it, which +// forces libstdc++'s globals_io.o into the link. That object's +// _GLOBAL__sub_I.00090_globals_io.cc initializer constructs cin/cout/cerr/clog +// (and the wchar_t variants) before main, dragging the full std::locale facet +// set (ctype/numpunct/moneypunct/timepunct/messages for char and wchar_t) into +// every Bun process startup. Bun never touches C++ iostreams at runtime. +// +// , , and are fine: they declare the +// stream types but do not emit the static Init object. If you need to print to +// stderr from C++, use fputs/fprintf. +// +// The upstream source of the original leak was the vendored simdutf header +// inside WebKit (Source/WTF/wtf/simdutf/simdutf_impl.h); that is handled by +// the WebKit pin. This test guards Bun's own compiled C++ so the initializer +// cannot creep back in through packages/ or src/. +test("C++ sources compiled into Bun do not include ", async () => { + const repoRoot = path.resolve(import.meta.dir, "..", "..", ".."); + + const roots = ["src", "packages/bun-uws", "packages/bun-usockets"]; + // sizegen.cpp is a build-time code generator, not linked into the bun binary. + const allowlist = new Set(["src/jsc/headergen/sizegen.cpp"]); + + const iostreamInclude = /^\s*#\s*include\s*/m; + const violations: string[] = []; + + for (const root of roots) { + let scanned = 0; + const glob = new Glob("**/*.{h,hpp,hxx,cpp,cc,cxx}"); + for await (const rel of glob.scan({ cwd: path.join(repoRoot, root) })) { + scanned++; + const relFromRepo = path.join(root, rel).replaceAll("\\", "/"); + if (allowlist.has(relFromRepo)) continue; + const source = readFileSync(path.join(repoRoot, root, rel), "utf8"); + if (iostreamInclude.test(source)) { + violations.push(relFromRepo); + } + } + // Guard against repoRoot resolving wrong (test file moved) or a scanned + // root going away, which would make the ban below pass vacuously. + expect(scanned).toBeGreaterThan(0); + } + + violations.sort(); + expect(violations).toEqual([]); +});