docs/reference/cpp-safety-review-checklist.md
These checkists are to be used when performing code reviews for memory safety, thread safety, and concurrency correctness. They are not exhaustive but cover common patterns and issues to look for. Always consider the specific context of the code being reviewed and apply judgment accordingly.
kj::Own, kj::Rc, kj::Maybe)util/weak-refs.h) or other techniques would be saferkj::ArrayPtr, kj::StringPtr, or other non-owning views into data owned by this (or by a parameter) are annotated with KJ_LIFETIMEBOUND. This expands to [[clang::lifetimebound]] and enables the compiler to warn when the returned
view outlives the object it borrows from.kj::Maybe, error indicators, or expensive-to-compute
results should be annotated with KJ_WARN_UNUSED_RESULT. This catches two classes of problems:
silently discarding results the caller must act on (e.g., error codes, kj::Promise), and
performing expensive computation whose result is thrown away.jsg::Ref<T> or other GC-traced references should use JSG_VISITABLE_LAMBDA
(see jsg/function.h) so V8's GC can trace through the captures.KJ_DISALLOW_COPY_AND_MOVE to prevent accidental moves that break scope invariants. Use
KJ_DISALLOW_COPY only when explicit move semantics are intentionally provided. Flag new
scope-guard or lock types that are missing these annotations.co_await should carry the KJ_DISALLOW_AS_COROUTINE_PARAM
annotation for compile-time enforcement. This is already applied to jsg::Lock, Worker::Lock,
jsg::V8StackScope, and similar types. Flag new lock or scope types that are missing it.jsg::Object instances) without use of IoOwn or IoPtr (see io/io-own.h) to ensure proper lifetime and thread-safety.kj::Refcounted is used when kj::AtomicRefcounted is required
for thread safety.Always watch for these concrete patterns. When you encounter these, flag them at the indicated severity.
Always watch for non-obvious complexity at V8/KJ boundaries, including:
CRITICAL / HIGH:
liftKj (see jsg/util.h). V8 callbacks must catch
C++ exceptions and convert them to JS exceptions.jsg::Object subclass storing kj::Own<T>,
kj::Rc<T>, kj::Arc<T> for an I/O-layer object without wrapping in IoOwn or IoPtr (see
io/io-own.h). Causes lifetime and thread-safety bugs.kj::Refcounted in cross-thread context: A class using kj::Refcounted whose instances can
be accessed from both the I/O thread and the JS isolate thread. Needs kj::AtomicRefcounted.co_await: Holding a jsg::Lock, V8 HandleScope, or similar V8
scope object across a coroutine suspension point. This is undefined behavior.co_await: Any RAII type or variable capturing
a raw pointer or reference used across a coroutine suspension point without kj::coCapture to
ensure correct lifetime.jsg::Object subclasses: Two or more jsg::Object subclasses holding
strong references (jsg::Ref<T> or kj::Own<T>) to each other without GC tracing via JSG_TRACE.
Causes memory leaks invisible to V8's GC. This includes transitive cycles (e.g., A holds B, B
holds C, C holds A) and cycles involving more than two objects.MEDIUM (safety-related):
.then() or stored for deferred execution
using [&] or [this] when only specific members are needed. Prefer explicit captures and
carefully consider captured variable lifetimes.ArrayBuffer backing
store creation, string flattening, v8::Object::New()) inside hot loops or time-sensitive
callbacks may trigger GC unexpectedly.DISALLOW_KJ_IO_DESTRUCTORS_SCOPE awareness: The DISALLOW_KJ_IO_DESTRUCTORS_SCOPE
macro (see jsg/wrappable.h) creates a scope that crashes the process if any KJ async/I/O object
is destroyed. It enforces that JS-heap objects use IoOwn/IoPtr rather than holding I/O objects
directly. IoOwn's destructor creates a matching AllowAsyncDestructorsScope to permit safe
destruction. When reviewing code that introduces new GC or destructor paths, verify these scopes
are correctly nested.noexcept(false) destructors. Do not
flag this as an issue unless there is a specific exception safety problem (e.g., double-exception
during stack unwinding).