Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,18 @@ bool_flag(
# =============================================================================
# I/O backend selection: a build/compile/link-time hermeticity guarantee.
#
# --//:io_backend=rust (default) -- the rust I/O layer: the process event loop is tokio
# (kj-rs-tokio) and every socket/stream is tokio-backed (kj-rs-io); the
# C++ layers above the streams (kj-http, kj-tls, capnp-rpc) are unchanged
# and run over those tokio streams. In this config kj's own OS-I/O
# layer (kj-async-os) must be ABSENT from the link: checked on the
# dependency graph by build/rust_io_graph_check.sh and on the linked
# --//:io_backend=rust (default) -- the rust I/O and HTTP/TLS layer: the process event loop is
# tokio (kj-rs-tokio), every socket/stream is tokio-backed (kj-rs-io),
# and kj's HTTP and TLS interfaces are implemented on hyper and rustls
# (kj-hyper). capnp-rpc remains the unchanged C++ implementation, running
# over tokio-backed streams. In this config kj's own OS-I/O layer
# (kj-async-os), kj's HTTP implementation (kj-http-impl) and kj-tls must
# be ABSENT from the link: checked on the dependency graph by
# build/rust_io_graph_check.sh, and for kj-async-os also on the linked
# binary by //src/workerd/server:rust-io-link-check.
# --//:io_backend=cxx -- workerd's I/O is the concrete C++ stack end to end, including the
# kj OS event loop and sockets. Byte-identical to the pre-migration
# build.
# --//:io_backend=cxx -- workerd's I/O is the concrete C++ stack end to end: kj-http, kj-tls
# and the kj OS event loop and sockets. Byte-identical to the
# pre-migration build.
#
# Code that must differ per backend keys off the WORKERD_RUST_IO_BACKEND_RUST define (see
# //src/workerd/util:setup-async-io) or off the config_settings below in a select().
Expand Down
36 changes: 36 additions & 0 deletions build/deps/rust.MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ crate.annotation(
crate = "capnpc",
gen_binaries = ["capnpc-rust"],
)

# Header-value leniency patches for the Rust HTTP path (kj-hyper / hyper): KJ, following the
# browsers' lead, forbids only NUL, CR, and LF in header values, while the `http` crate's
# HeaderValue constructors and `httparse`'s wire parser additionally reject the other C0
# control bytes and DEL. workerd must round-trip such values byte-for-byte in both directions
# (WPT fetch/api headers/header-values*.any.js asserts exactly this), so both validators are
# relaxed to KJ's rule. CR/LF stay forbidden, so header injection/splitting stays impossible.
crate.annotation(
crate = "http",
patch_args = ["-p1"],
patches = ["//:patches/rust/http-kj-lenient-header-values.patch"],
)
crate.annotation(
crate = "httparse",
patch_args = ["-p1"],
patches = ["//:patches/rust/httparse-kj-lenient-header-values.patch"],
)

# Header-name case fidelity: kj-http writes header names in exactly the case the application
# supplied, and workerd's tests (and some peers) assert the raw bytes. hyper's encoder already
# honors a per-message HeaderCaseMap extension (falling back to title-casing), and its parser
# records peers' spellings in one, but upstream keeps the type private; this patch makes it
# public so kj-hyper can fill it for outgoing messages and read it for incoming ones.
#
# Graceful shutdown keeps a connection with a partially received request open until that request
# is answered, as kj's HttpServer::drain() does; upstream closes it as idle. And a client keeps a
# connection out of the pool until the response body is consumed, as kj's HttpClient does.
crate.annotation(
crate = "hyper",
patch_args = ["-p1"],
patches = [
"//:patches/rust/hyper-public-header-case-map.patch",
"//:patches/rust/hyper-graceful-shutdown-partial-request.patch",
"//:patches/rust/hyper-client-lazy-response-body.patch",
],
)
crate.from_cargo(
# TODO(cleanup): This name is a bit misleading now that we don't vendor. We can clean it up as a
# followup.
Expand Down
27 changes: 25 additions & 2 deletions build/fixtures/rust_io_link_check.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/bin/bash
# Link-truth check for the Rust I/O backend: inspects the symbol names of the linked workerd
# binary and fails if kj's own C++ event-loop setup made it into the link.
# binary and fails if kj's own C++ event-loop setup, HTTP implementation or TLS made it into the
# link.
#
# Under --//:io_backend=rust, kj::setupAsyncIo() is supplied by //src/workerd/util:setup-async-io
# (tokio-backed) by symbol override, and the concrete kj OS I/O layer (kj-async-os) must not be
Expand All @@ -12,6 +13,10 @@
# * kj's setupAsyncIo() has a local class `BasicContext`, and kj's async-io-unix.c++ has
# `LowLevelAsyncIoProviderImpl`; neither may be present.
# * the shim's TU references kj_rs_io::TokioAsyncIoContext; it must be present.
# * kj's HTTP/1.1 implementation (kj-http-impl) has `kj::HttpServer::Connection`, and kj-tls
# has `kj::TlsContext`; neither may be present. kj-hyper's namespace must be: the kj::
# HTTP entry points are defined over it (//src/workerd/util:kj-http), and with static archives
# a stray kj-http-impl could otherwise win the link silently.
# The kj::UnixEventPort::* symbols ARE expected: the shim defines an inert UnixEventPort because
# kj::AsyncIoContext names the type (see setup-async-io-tokio.c++).
#
Expand All @@ -38,13 +43,16 @@ BIN="${TEST_SRCDIR:-.}/$1"
PAT_KJ_SETUP='12setupAsyncIoEvEN?12BasicContext' # kj::setupAsyncIo()::BasicContext (+ members)
PAT_KJ_LOWLEVEL='28LowLevelAsyncIoProviderImpl' # kj::(anon)::LowLevelAsyncIoProviderImpl
PAT_SHIM='8kj_rs_io19TokioAsyncIoContext' # kj_rs_io::TokioAsyncIoContext
PAT_KJ_HTTP='N2kj10HttpServer10Connection' # kj::HttpServer::Connection
PAT_KJ_TLS='N2kj10TlsContext' # kj::TlsContext
PAT_HYPER='8kj_hyper' # workerd::rust::kj_hyper
PAT_UNIXPORT='N2kj13UnixEventPort' # kj::UnixEventPort
PAT_ANY_KJ='N2kj' # any kj:: symbol at all

# One pass over the (large, debug-build) binary, extracting just the matching names; count
# from that small file. LC_ALL=C: byte-wise matching, no multibyte decoding of binary data.
SYMS="${TEST_TMPDIR:-/tmp}/rust-io-link-check.syms"
{ LC_ALL=C grep -a -o -E "$PAT_KJ_SETUP|$PAT_KJ_LOWLEVEL|$PAT_SHIM|$PAT_UNIXPORT|$PAT_ANY_KJ" "$BIN" || true; } > "$SYMS"
{ LC_ALL=C grep -a -o -E "$PAT_KJ_SETUP|$PAT_KJ_LOWLEVEL|$PAT_SHIM|$PAT_KJ_HTTP|$PAT_KJ_TLS|$PAT_HYPER|$PAT_UNIXPORT|$PAT_ANY_KJ" "$BIN" || true; } > "$SYMS"
count() { { grep -c -E "$1" "$SYMS" || true; } | tr -d ' '; }

kj_total=$(count "$PAT_ANY_KJ")
Expand All @@ -59,12 +67,18 @@ fi
kj_setup=$(count "$PAT_KJ_SETUP")
kj_lowlevel=$(count "$PAT_KJ_LOWLEVEL")
shim=$(count "$PAT_SHIM")
kj_http=$(count "$PAT_KJ_HTTP")
kj_tls=$(count "$PAT_KJ_TLS")
hyper=$(count "$PAT_HYPER")
unixport=$(count "$PAT_UNIXPORT")

echo "rust-io-link-check: $BIN"
echo " kj setupAsyncIo()::BasicContext names : $kj_setup (must be 0)"
echo " kj LowLevelAsyncIoProviderImpl names : $kj_lowlevel (must be 0)"
echo " shim kj_rs_io::TokioAsyncIoContext : $shim (must be > 0)"
echo " kj HttpServer::Connection names : $kj_http (must be 0)"
echo " kj TlsContext names : $kj_tls (must be 0)"
echo " kj-hyper names : $hyper (must be > 0)"
echo " kj::UnixEventPort::* names : $unixport (informational; the shim's inert port)"

status=0
Expand All @@ -76,6 +90,15 @@ if [ "$kj_setup" -ne 0 ] || [ "$kj_lowlevel" -ne 0 ]; then
echo " and retarget it to :kj-async-core / :kj-async-io."
status=1
fi
if [ "$kj_http" -ne 0 ] || [ "$kj_tls" -ne 0 ]; then
echo "FAIL: kj's HTTP implementation or kj-tls is linked into the rust-backend binary. Find the"
echo " offending edge with: just check-io-backend-graph"
status=1
fi
if [ "$hyper" -eq 0 ]; then
echo "FAIL: kj-hyper (//src/workerd/util:kj-http's implementation) is not in the link."
status=1
fi
if [ "$shim" -eq 0 ]; then
echo "FAIL: the tokio setupAsyncIo() shim (//src/workerd/util:setup-async-io) is not in the link."
status=1
Expand Down
12 changes: 7 additions & 5 deletions build/rust_io_backend.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

The dependency side of the backend switch lives in //src/rust/cxx/kj-rs-io:active-backend (one
select over the per-backend deps); this file carries the compile-time side: the define read by
the two translation units that have a per-backend arm.
the translation units that have a per-backend arm.

Hermeticity of the rust config (kj-async-os, kj's own event loop and sockets, must be ABSENT from
the workerd link, or its kj::setupAsyncIo() / kj::UnixEventPort definitions collide with the tokio
shim's and the linker silently keeps whichever archive it meets first) is checked in two places:
* build/rust_io_graph_check.sh -- one `bazel cquery somepath(...)` over the dependency graph
Hermeticity of the rust config is checked in two places. kj-async-os (kj's own event loop and
sockets) must be ABSENT from the workerd link, or its kj::setupAsyncIo() / kj::UnixEventPort
definitions collide with the tokio shim's and the linker silently keeps whichever archive it meets
first; likewise kj-http-impl (kj's HTTP/1.1 implementation, whose kj:: symbols
//src/workerd/util:kj-http defines over hyper) and kj-tls (replaced by rustls).
* build/rust_io_graph_check.sh -- `bazel cquery somepath(...)` over the dependency graph
(`just check-io-backend-graph`; the lint CI lane runs it);
* //src/workerd/server:rust-io-link-check -- the linked binary's symbol names.
"""
Expand Down
50 changes: 33 additions & 17 deletions build/rust_io_graph_check.sh
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
#!/bin/bash
# Dependency-graph check for the Rust I/O backend: under --//:io_backend=rust, the workerd binary
# must not reach kj-async-os (kj's own event loop and sockets), directly or through the
# @capnp-cpp//src/kj:kj-async umbrella. If it does, kj::setupAsyncIo() and kj::UnixEventPort's
# members are defined twice -- by kj and by the tokio shim (//src/workerd/util:setup-async-io) --
# and with static archives the linker keeps whichever it meets first, silently.
# must not reach
# * kj-async-os (kj's own event loop and sockets), directly or through the
# @capnp-cpp//src/kj:kj-async umbrella: kj::setupAsyncIo() and kj::UnixEventPort's members
# would be defined twice -- by kj and by the tokio shim (//src/workerd/util:setup-async-io);
# * kj-http-impl (kj's HTTP/1.1 implementation), directly or through the kj-http umbrella: its
# kj:: symbols are defined over hyper by //src/workerd/util:kj-http;
# * kj-tls (OpenSSL), replaced by rustls.
# With static archives the linker keeps whichever definition it meets first, silently.
#
# One query, empty output means clean; otherwise it prints one offending dependency path. Extra
# One query per forbidden target; empty output means clean, otherwise it prints one offending
# dependency path. Extra
# arguments are passed to bazel (CI passes its --config flags). The linked binary's symbols are
# checked separately by //src/workerd/server:rust-io-link-check.
set -euo pipefail
cd "$(dirname "$0")/.."

TARGET=//src/workerd/server:workerd
FORBIDDEN=@capnp-cpp//src/kj:kj-async-os
FORBIDDEN=(
"@capnp-cpp//src/kj:kj-async-os|Retarget the offending edge from the @capnp-cpp//src/kj:kj-async umbrella to :kj-async-core / :kj-async-io."
"@capnp-cpp//src/kj/compat:kj-http-impl|Depend on //src/workerd/util:kj-http (or :kj-http-types) instead of @capnp-cpp//src/kj/compat:kj-http."
"@capnp-cpp//src/kj/compat:kj-tls|Use //src/workerd/server:tls-network instead of kj-tls."
)

errlog=$(mktemp)
trap 'rm -f "$errlog"' EXIT
if ! paths=$(bazel cquery "$@" --//:io_backend=rust "somepath($TARGET, $FORBIDDEN)" 2>"$errlog"); then
cat "$errlog" >&2
exit 1
fi
if [ -n "$paths" ]; then
echo "FAIL: $TARGET reaches $FORBIDDEN under --//:io_backend=rust:"
echo "$paths"
echo "Retarget the offending edge from the @capnp-cpp//src/kj:kj-async umbrella to :kj-async-core / :kj-async-io."
exit 1
fi
echo "ok: $TARGET does not reach $FORBIDDEN under --//:io_backend=rust"
status=0
for entry in "${FORBIDDEN[@]}"; do
forbidden=${entry%%|*}
hint=${entry#*|}
if ! paths=$(bazel cquery "$@" --//:io_backend=rust "somepath($TARGET, $forbidden)" 2>"$errlog"); then
cat "$errlog" >&2
exit 1
fi
if [ -n "$paths" ]; then
echo "FAIL: $TARGET reaches $forbidden under --//:io_backend=rust:"
echo "$paths"
echo "$hint"
status=1
else
echo "ok: $TARGET does not reach $forbidden under --//:io_backend=rust"
fi
done
exit $status
8 changes: 8 additions & 0 deletions deps/rust/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ cc_library(
linkopts = select({
"@platforms//os:windows": [
"ntdll.lib",
# System certificate store APIs behind rustls-platform-verifier.
"crypt32.lib",
],
"@platforms//os:macos": [
"-framework",
"Security",
"-framework",
"CoreFoundation",
],
"//conditions:default": [],
}),
Expand Down
Loading
Loading