Repository navigation
Error contract P1: jinja2::Exception, one-pointer ErrorInfo, codes 15-18, Load/Render boundary (0253) - #597
Conversation
…253)
- jinja2::Exception (exception.h): std::runtime_error with Code() and the
C++ origin of the throw (__builtin_FILE/__builtin_LINE), exported, key
function out of line.
- ErrorCode 15-18 (UserCallableException, IntegerOutOfRange,
TemplateRuntimeError, ResourceLimitExceeded) with ToString texts;
jinja2::error_category(), is_error_code_enum and make_error_code, so
FileNotFound/TemplateNotFound compare equal to errc::no_such_file_or_directory.
- BasicErrorInfo is one pointer to a shared immutable payload that also
keeps the original exception (GetException()); copies and moves and all
getters are noexcept. The wide conversion keeps relatedLocs and the
exception.
- Value(I) and the integral Reflector throw
jinja2::Exception{IntegerOutOfRange} from an out-of-line
detail::ThrowIntegerOutOfRange; new Value::TryFrom(I, Value&) noexcept.
- BadResultAccess<CharT>; config.h #error without exceptions.
- Load, Render, GetMetadata and TemplateEnv::LoadTemplate: std::bad_alloc
propagates; std::length_error and string/list repeat caps become
ResourceLimitExceeded; host jinja2::Exception keeps its code; anything
else is UnexpectedException with the exception kept.
- LoadFromFile of a missing file returns FileNotFound; ToChars reports
invalid_argument for non-allocation failures.
- Allocation-failure injection test (new jinja2cpp_alloc_tests binary)
and the bugs it found: a NodeArena block listed before its storage
existed, robin_hood inserts, growth and copies that left a broken table
after a throw (third local change, documented in the header and SBOM),
and IsFilterName swallowing bad_alloc.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
…a copies before expected, Windows DLL skip Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
… (0279) nlohmann::basic_json's destructor allocates for non-empty containers, so an allocation failure there terminates; filed as task 0279. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
…0279) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QEfqEiJoJF5wCn3xZSp8zW
…tests last A header borrows the compile flags of the most similar database entry, and in CI include/jinja2cpp/exception.h picks Boost's filesystem/src/exception.cpp, which lacks build/generated/include. The allocation tests move after the parity run so their output stays in the Windows log tail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
Their runtimes define the global operator new, so the replacements in allocation_failure_test.cpp fail to link there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
…oundary Engine limits throw ResourceLimitError (a std::length_error), which the boundary reports as ResourceLimitExceeded without GetException(), as docs/api-2.0.md 2.1 now says; a host std::length_error still keeps its exception. Files 0282 (cycle leaks when an allocation fails mid-render) and records 0253 in progress. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
…ss dies Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
MSVC debug containers allocate a proxy in noexcept constructors such as basic_string(), so failing one is std::terminate in the test's own code (the MSVC C++23 Debug job's stack: Load's default name argument). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
…structors are MSVC's std::list and std::unordered_map allocate a sentinel when default constructed, so the swap-based move threw bad_alloc through noexcept and terminated (alloc test, Load of a filter call with keyword arguments). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
…uctor does not allocate ~BlocksStack emplaced SelfBlocks::ended, which default-constructs an unordered_map; MSVC allocates there, so a failed allocation threw out of the destructor and terminated (alloc test, Render of a template with blocks). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
|
@codex review @codex security review (this PR changes the Load/Render error boundary, the loaders' file handling and the evaluator's limit errors) Generated by Claude Code |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex security review Generated by Claude Code |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…0279) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QEfqEiJoJF5wCn3xZSp8zW
Value(I) now stores a BigInt outside int64 instead of throwing, so Value::TryFrom and detail::ThrowIntegerOutOfRange are dropped. A BigInt that reaches a template throws IntegerOutOfRangeError (src/engine_errors.h, was resource_limit.h): code 16 with no exception, as an engine error. The autoescape selector runs inside the Load boundary. Exception joins fwd.h and jinja2cpp.h; task 0253 is done. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
…ne-unhandled-self-assignment) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
|
@codex review (After merging master: Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 862ae2e49c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…); REVIEW.md names engine_errors.h The selector ran inside Load's try, so an ErrorInfo it threw was taken for the parser's own and returned as is. It now runs in its own try that goes through CurrentExceptionToError. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
One ResourceLimitError (src/engine_errors.h) serves Settings::Limits, named by its field, and the built-in caps, without one; CurrentExceptionToError reports both as ResourceLimitExceeded with the field as extraParams[1] when present, so Render's own catch for it is gone. The built-in sequence cap throws it instead of std::length_error. exception.h, engine_errors.h and error_boundary.h are wrapped in v2 by scripts/abi_namespace.py. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
#601 brought TemplateDict with defaulted noexcept moves over an OrderedMap, whose moves allocate on MSVC; a bad_alloc there in a dict literal's render terminated (jinja2cpp_alloc_tests, Windows static jobs). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
Load rose 0.4-1.4% on every case at 92b2896, all in Allocate: the exception-safe new-block path and the ResourceLimitError message string were built inline into it. Both are now out of line (AddBlock, ThrowTooLarge); Load is 1-2.5% below 5fe00b9 on every case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTQTriEsFrrBgcYNKDUUF4
Sync the task index after #597
Requested by Ruslan · project thread
Before: a host callable that threw
intescapedRender;std::bad_allocbecameUnexpectedException; a host had no way to report a specific code or get its exception back;ErrorInfowas ~128 B by value;LoadFromFile("missing")returned success; aBigIntthat reached a template failed asUnexpectedException. Abad_allocin the middle of Load or Render could leave a broken arena or hash table behind, or terminate the process on MSVC (crashes found by the new test).After: Load, Render,
GetMetadata*andTemplateEnv::LoadTemplate*return every failure inResult, and onlystd::bad_allocescapes, with the template still usable. A host throwsjinja2::Exception{code}and gets that code back; any other host exception arrives asUnexpectedExceptionwith the original kept inErrorInfo::GetException(). Errors the engine raises (its limits, aBigIntit cannot hold yet) carry their code and no exception.ErrorInfois one pointer.ErrorCodeworks withstd::error_code.This is phase P1 of the 0250 error contract (docs/api-2.0.md §2.1, decisions D1-D10 approved 2026-10-18). P2 (render locations, 0254) and P3 (engine error codes, 0255) change no public header. Task 0253 is marked done here.
How.
include/jinja2cpp/exception.h(new, infwd.handjinja2cpp.h): exportedjinja2::Exception : std::runtime_errorwithCode()and the C++ origin (File()/Line()from__builtin_FILE/__builtin_LINEdefault arguments, one layout at every standard). The destructor is out of line insrc/error_info.cpp(key function).ErrorCode15UserCallableException, 16IntegerOutOfRange, 17TemplateRuntimeError, 18ResourceLimitExceeded, withToStringtexts.jinja2::error_category(),make_error_code,std::is_error_code_enum;FileNotFound/TemplateNotFoundcompare equal toerrc::no_such_file_or_directory, 16/18 tovalue_too_large,InvalidValueType/InvalidTemplateNametoinvalid_argument.BasicErrorInfoholds one intrusive-refcounted pointer to an immutable payload.Datagains a trailingstd::exception_ptr exception(aggregate init stays source compatible).BasicErrorInfo(Data)and every getter keep their signatures and are nownoexcept, as are copy and move. NewGetException(). The wide conversion keepsrelatedLocsand the exception.template.h:BadResultAccess<CharT>.config.h:#errorwithout exceptions; C4275 joins the MSVC suppression macro.src/error_boundary.h:CurrentExceptionToErrorclassifies the exception being handled (bad_allocrethrown;jinja2::Exceptionkeeps its code;std::length_erroris 18; anything else, including non-std::exceptiontypes, is 1). Used byTemplateImpl::Load(the autoescape selector from Markup from C++: jinja2::Markup / WMarkup in Value (0180) #590 runs inside it; a selector that throws is a host exception),Render,GetMetadataandTemplateEnvImpl::LoadTemplate(filesystem handlers).src/engine_errors.h): the engine's own limits (string and list repeat caps, the sequence size check, the arena caps, templates per render) throwResourceLimitError(astd::length_error), reported as 18; aBigIntreaching a template (until 0270) throwsIntegerOutOfRangeError(astd::overflow_error), reported as 16. Neither keepsGetException(), as docs/api-2.0.md 2.1 says for errors the engine raises. AllHugeSequencesFailFastrows now report 18 (previously 1). A hoststd::length_erroris also 18 and keeps its exception. After merging Settings::Limits: bounds on what one Load or Render may do (0262) #601 oneResourceLimitErrorserves bothSettings::Limitsand the built-in caps: aSettings::Limitserror names its limit inExtraParams[1], a built-in cap names none, andRender's separate catch for it is gone.Value::TryFromanddetail::ThrowIntegerOutOfRange. WithBigInt(0269)Value(I)stores an integer outside int64 instead of throwing, so the sibling could only fail onbad_alloc. docs/api-2.0.md updated to match.LoadFromFileof a missing file returnsFileNotFound.ToCharsreportsinvalid_argumentunless the failure wasbad_alloc.src/error_info.cppare ininline namespace v2(Inline ABI namespace jinja2::v2 and its release gates (0076 PR-2) #604,scripts/abi_namespace.py --checkpasses).Result" rule now describes this boundary.Allocation-failure injection (the R2 proof obligation, plan §9.5).
test/allocation_failure_test.cppis a separate binary,jinja2cpp_alloc_tests, because it replaces the globaloperator newand would otherwise hide the sanitizers' own from every other test. It fails the Nth allocation, for every N, on a fresh thread so per-thread pools start cold. It covers Load, Render and GetMetadata of six templates: includes, imports, macros, extends, filters, a callable and dict literals. Each run must succeed with the right output or throwbad_alloc, and the template must work again afterwards. When the process dies it prints the failing allocation number (and, on MSVC C++23, a stack). It never fails MSVC's 16-byte checked-iterator proxy allocations, which Debug builds make insidenoexceptstandard constructors. It found these bugs, fixed here:NodeArena::Allocatelisted a new block before its storage existed, so~NodeArenawalked a null block (segfault in Load).operator[],try_emplaceandinsert_or_assignreserved the slot before building the node;initDatachanged the table before allocating; copies left half-built tables; a failed rehash kept a new hash multiplier (the verifier found that one). The fixes are a third documented local change insrc/robin_hood.h, recorded in the SBOM. 0231's successor map must pass this test too.IsFilterNameand the nlohmann metadata parser swallowedbad_alloc, andGetMetadata/GetMetadataRawcopied into expected-lite'snoexceptconverting constructor, wherebad_allocmeantstd::terminate.std::listandstd::unordered_mapallocate when default constructed.OrderedMap's swap-based move wasnoexcept, so Load of a call with keyword arguments terminated; its moves are nownoexceptonly where both containers' default constructors are (stillnoexcepton libstdc++ and libc++).TemplateDict(from master) declared defaultednoexceptmoves over anOrderedMap, so Render of a dict literal terminated; they now followOrderedMap's.~BlocksStackemplaced the ended blocks of a template'sself, so Render of a template with blocks terminated;SelfBlocks::endedis now made with theself, and the destructor only moves into it.Where it does not run: on a Windows DLL build the library uses its CRT's
operator new, so the tests skip. The TSan and MSan runtimes defineoperator newthemselves, so those builds do not build the binary. Withjson=nlohmannthe metadata row skips:nlohmann::basic_json's destructor allocates for non-empty arrays and objects, so a failure there terminates inside anoexceptdestructor; that is upstream behaviour, filed as task 0279 (boostandrapidare unaffected). A reference-cycle row (0110's collector) passes but leaks under LSan when an allocation fails mid-render; it is left out and filed as task 0283.Tests.
ErrorContractTestin errors_test.cpp: static_asserts on codes 15-18,sizeof(ErrorInfo) == sizeof(void*), noexcept copy, move and getters,is_base_of<runtime_error, Exception>andBadResultAccess. Behaviour rows: a callable throwing a non-std type gives 1 and rethrows it;bad_allocpropagates;length_errorgives 18; a hostjinja2::Exception{UndefinedError}gives 11; a throwing list accessor; a throwing autoescape selector;Exceptionorigin; aBigIntin a template (direct and reflected) gives 16 with no exception;error_codeequivalence; payload sharing and the wide conversion;LoadFromFilemissing (narrow and wide); a throwing filesystem handler (narrow, wide, through an include);ToChars.HugeSequencesFailFastchecks that engine limits carry no exception. BigInt rows in basic_tests.cpp expect 16 where master expected 1. fwd_test.cpp declares a function overjinja2::Exceptionthroughfwd.h.Roles and rounds. Main session implemented;
ci-triageread the red jobs. Verifier: 1 round. It ran clang-18 C++20 with nlohmann, a GCC shared build, ASan+UBSan, its own robin_hood and allocation fuzz harnesses, TSan onErrorInfocopies, and C++17 header self-containment. Two blocking findings, both fixed: the robin_hood rehash multiplier, and the alloc test on a Windows DLL. It also found the noexceptexpectedconversion inGetMetadata, fixed. Local, after merging #601 and #604: ctest 5/5 on GCC Debug, on clang-18 ASan+UBSan and on GCC Release shared json=nlohmann;scripts/preflight.sh --perfpasses (instruction gate within +3% on every benchmark). Red CI pushes: 8. (1) dcaffcf: the nlohmann jobs failed the metadata allocation row (the parser's catch-all turnedbad_allocintoMetadataParseError; fixed in 1b5a620, 0279 filed). clang-tidy failed becauseinclude/jinja2cpp/exception.hborrowed the flags of Boost'sfilesystem/src/exception.cpp, which lack the generated include dir; the workflow and preflight now pass it. TSan failed to link the alloc tests, which are now not built there. Both Windows static jobs failedjinja2cpp_alloc_testswith its output past the log tail; it now runs after the parity run so the output is visible. The local runs had used only the boost binding, no TSan, and dependencies outsidebuild/_deps. (2) daeed23 never ran: master had moved and the PR conflicted. (3-6) 24e843b, fa92426, fdcb9c4 and 7358fe0: the two Windows static jobs aborted injinja2cpp_alloc_tests, each push getting one step further (crash report, the Debug proxy allocations,OrderedMap's move,~BlocksStack). The cloud cannot build with MSVC, so each step needed a CI run. (7) 520411c (the first master merge): clang-tidybugprone-unhandled-self-assignmentonErrorInfo's copy-and-swap through a temporary, newly on changed lines against the new base; preflight had reported it, and the push went out anyway. Fixed in 862ae2e, green on every job. (8) 685cf51 (the second master merge, #601 and #604): both Windows static jobs aborted injinja2cpp_alloc_testson the dict-literal row, in master'sTemplateDictnoexceptmove (MSVC only, so preflight could not see it); fixed in 92b2896. Codex: 2 findings, 2 fixed, 0 declined (a P2 and a P3 on 862ae2e, fixed in 408d958;@codex security reviewwas requested twice but each request ran a code review, so no security review result).Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_013sRZrdyAGVT1hBgARAkNAE