Skip to content

CefPostDataElement.create() crashes if it's the first native CEF object created in the process (Release build) #16

Description

@Thrameos

Summary

CefPostDataElement.create() crashes with a native FATAL if it happens to be the very first native CEF object created in the process (i.e. before any CefBrowser has ever been created) -- and this reproduces in the Release build, not just the ENABLE_COVERAGE Debug build issue #9 already tracks.

FATAL:cef/libcef_dll/cpptoc/post_data_element_cpptoc.cc:171] CefPostDataElement_0_CppToC called with invalid version -1

This is the same class of bug as issue #9's "Crash 1" (CefPrintSettings.create() as the first native object, print_settings_cpptoc.cc:452), which was documented there as Debug-build-only. This finding shows the underlying issue is broader: it's not specific to the Debug/coverage build, just specific to being the first native CEF object created, regardless of build type.

How this was found

Not a deliberate repro attempt -- discovered by accident while working on Track B (real native coverage measurement, see plan/roadmap.md). Splitting one test method out of CefPostDataTest into a new class (to let the rest of that class rejoin an ENABLE_COVERAGE Debug-build coverage run that a different method's crash was blocking) changed JUnit5's default test-class execution order enough that the new class became the very first test class to run in the entire suite -- meaning CefPostDataElement.create() became the first native CEF call in the process. This crashed the Release build outright, confirmed reproducible twice in a row (deterministic given that exact class list, not flaky).

This also means the existing full suite's current test order is silently load-bearing for correctness: if any other future change to this suite (new test class, renamed class, different JUnit discovery order) happens to promote a value-object-only test class (one that never creates a browser) to run first, the whole suite could start crashing outright with no warning. That's the more concerning part of this finding, independent of the coverage-measurement work that surfaced it.

Repro

Added java/tests/junittests/CefPostDataElementFirstNativeObjectTest.java (currently @Disabled with a link to this issue, so it doesn't destabilize the normal suite -- remove @Disabled and run it via --select-class alone to reproduce; do NOT remove @Disabled while it's part of a suite that also contains other tests, since which test runs first is exactly the variable that matters here). Confirmed crashing:

  1. In complete isolation (--select-class tests.junittests.CefPostDataElementFirstNativeObjectTest, nothing else selected) -- guarantees it's the first (and only) native call in the process.
  2. Reproducible twice in a row, not order-flaky.

Fix sketch (not implemented here)

Likely the same underlying root cause as issue #9's Crash 1 -- some CEF API struct's embedded version field is only correctly initialized after a real browser has been created first. Given this now provably affects Release too, this deserves investigation independent of the Debug-only framing issue #9 originally gave it. A workaround at the JCEF harness level (e.g. TestSetupExtension creating and disposing a throwaway browser during startup, before any @Test runs) would paper over it for this fork's own test suite, but wouldn't fix it for real embedding applications that happen to construct one of these value objects early.

Found via

This fork's coverage-expansion effort (tracked in #5), specifically while planning Track B improvements (recovering test coverage lost to whole-class exclusions around known Debug-build crashes) per the user's direction to work toward a higher coverage percentage.

Activity

  1. Thrameos commented on Aug 30, 2026

    @Thrameos
    OwnerAuthor

    Traced this further by reading CEF's own generator source (`~/devel/cef/tools/make_cpptoc_impl.py`) and the real API-versioning implementation it generates code against (`libcef_dll2.cc`), rather than guessing. Concrete findings:

    The mechanism: every ctocpp/cpptoc constructor for a versioned API type (one with multiple API-version variants -- i.e. its shape changed across CEF releases) gets generated code like:

    const int version = cef_api_version();
    LOG_IF(FATAL, version < N || version >= M) << __func__ << " called with invalid version " << version;

    `cef_api_version()` just returns a process-global `g_version`, which is set exactly once by `cef_api_hash(CEF_API_VERSION, 0)` -- called from `CefInitialize()`/`CefExecuteProcess()` in `libcef_dll_wrapper.cc` (our own linked wrapper code, shipped as source in the binary distribution). So by the time any of our tests run (well after `CefApp.getInstance()` triggers full `CefInitialize()`), `g_version` should already be set correctly, process-wide.

    Ruled out thread-affinity: `CefPrintSettings_N.cpp`'s `N_Create` calls `CefPrintSettings::Create()` directly on whatever thread the JVM calls it from (no `CefPostTask(TID_UI, ...)` dispatch) -- but so does `CefRequest_N.cpp`'s `N_Create`, called identically, from the identical (JUnit test) thread, and `CefRequest.create()` never crashes even as the literal first native call. So this isn't simply "wrong thread." The differentiator must be that `CefRequest` doesn't have multiple API-version variants (so no version-check code gets generated for it at all), while `CefPrintSettings`/`CefPostDataElement` do.

    Also confirmed: warming up with an unrelated native call first (a plain `CefRequest`, and separately a full real browser+page-load) does not prevent the crash -- it's specific to being the first `CefPrintSettings`-type (or `CefPostDataElement`-type) object, not "first native call in the process" generally, and not fixable by warming up with a different type.

    Not fully resolved: haven't pinned down why `cef_api_version()` reads -1 specifically for these types despite `g_version` apparently being set correctly earlier -- `g_version`/`current_version_hash` in `cef_api_hash()` are plain (non-atomic) globals with a "initialize on first successful lookup" pattern and no explicit memory barrier, which is a plausible (but unconfirmed) structural weakness, though the crash reproduces 100% deterministically rather than intermittently, which argues against a classic data race explanation. Whoever picks this up next should start by checking whether `cef_api_hash()` is actually being called successfully via some path before `CefPrintSettings::Create()`/`CefPostDataElement::Create()` specifically (vs. for other versioned types, if any exist in this fork's surface), e.g. with a debug print or breakpoint in `cef_api_hash()` itself.

    Also relevant: CEF's own internal test suite (`~/devel/cef/tests/ceftests/print_unittest.cc`) does test `CefPrintSettings::Create()` standalone via a plain gtest (`PrintTest.SettingsSetGet`) with no version issue reported there -- but that's a completely different process/binary (a dedicated `ceftests` gtest executable that presumably calls `CefInitialize`-equivalent setup once, single-suite, no other object types constructed first), so it doesn't tell us much about whether the same "first-of-versioned-type" pattern would trip there too if tried.

  2. Thrameos commented on Sep 1, 2026

    @Thrameos
    OwnerAuthor

    Fixed in commit d5b3595 (branch coverage/phase1-value-objects-phase2-handlers).

    Root cause confirmed: this repo's real default (windowless_rendering_enabled=true, external_message_pump mode) drives CEF's browser-process/IO-thread startup only via explicit doMessageLoopWork() calls -- some CEF-internal state (whatever populates the version field these *_cpptoc.cc wrappers check) only actually finishes initializing as a side effect of creating a browser. Confirmed via a pure-C++ repro (tools_native/leak_probe.cc, tools_native/null_param_repro/) that the equivalent CEF calls do NOT crash there -- but that repro deliberately uses multi_threaded_message_loop=true, letting CEF drive its own thread/pump instead.

    TestSetupExtension already had the right fix pattern for the isolated leak-sweep case (warmUpBrowserProcess() -- create and close one throwaway browser first) but it was gated to only run there. Made it unconditional, so every test run (including a single class selected in isolation via --select-class) starts from the same precondition every real embedding app already satisfies.

    Verified: CefPostDataElementFirstNativeObjectTest (this issue's own regression test) now passes 3/3 in complete isolation. Full suite still passes clean, 187/187 (no regression from the extra warmup browser cycle).

    Closing.

  3. added 2 commits that reference this issue on Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions