Skip to content

Offer: GitHub Actions CI (build/test/coverage) + 9 bugs found and fixed along the way -- no obligation, cherry-pick as you like #542

Description

@Thrameos

The offer, up front

I didn't see a .github/workflows/ setup here, so while building coverage tooling
on a personal fork I put together a GitHub Actions CI setup (Linux/macOS/Windows
builds, JUnit tests, JaCoCo+LLVM coverage, CodeQL) in case it's useful. Along the
way it surfaced 9 real bugs, all fixed, verified against a live CI run with all
checks green and a processed Codecov report.

No action is required from anyone here. Everything below is in one squashed,
CI-validated commit at Thrameos#35, with the full, unsquashed commit-by-
commit history preserved at the backport/phase1-ci-infra-full-history tag in the
same fork. Whatever's useful is available: merge the PR as-is if you want the whole
thing, hand it to a volunteer to review at whatever pace works, or cherry-pick
whichever individual fixes are wanted straight out of the full-history tag and
ignore the rest. Just flagging what's there.

Enhancements

  • GitHub Actions CI: build jobs for Linux/macOS/Windows, a JUnit test job, a
    coverage job, and CodeQL analysis (C++ and Java).
  • ENABLE_LLVM_COVERAGE: a Clang source-based native coverage CMake option.
    gcov-style instrumentation was tried and rejected during development --
    its .gcda writer isn't fork-safe against CEF's zygote-style process
    model, and silently produces wrong (usually near-zero) numbers for
    whichever files a test happens to hit a fork on -- so it was never carried
    into the branch's shipped configuration.
  • JaCoCo wired into the Java JUnit run (pinned to 0.8.15 for JDK 25 class-file
    support -- 0.8.12 can't parse it).
  • A CI wrapper (tools/run_tests_ci.sh / tools/run_coverage_ci.sh) that tells
    a real, reportable JUnit test failure apart from a known, already-diagnosed
    native crash: a real failure is never silently retried, while a known crash
    signature is retried within a bounded budget, or accepted as passing once
    it's been verified that the crash doesn't lose any test or coverage data.
    The point is to avoid two opposite failure modes -- a flaky job that's
    ignored because it's "always red," and a job that looks green by quietly
    swallowing real failures.
  • A leak-checker / process-isolation test harness (LeakChecker, LeakTargets,
    IsolatedRunner) for RSS-growth-based leak sweeps, plus roughly 25 tests ported
    from CEF's own ceftests suite and other coverage work (value objects, browser/
    context/request/response/download-item coverage, OSR/windowed smoke tests, drag-
    data, cookies, message router, print/PDF settings, etc.).
  • Current baseline on this branch: ~38% combined coverage.

Bugs found and fixed

  1. CefMessageRouter leak on router removal. RemoveMessageRouter() never
    called CancelPending() on outstanding persistent-query callbacks, which hold
    a CefRefPtr back to the router. If a browser is removed mid-query rather than
    actually closing, nothing else ever releases that reference -- the router, and
    transitively the browser context it keeps alive, leaks for the process's life.
    This looks like the same issue Preserve persistent query callbacks #541 and Support persistent & binary queries/callbacks #528 are addressing.
  2. CefRequestContext double-caching leak. getGlobalContextNative() was
    missing a final else, so every call after the first cached (and leaked) a
    second native reference.
  3. Global CefRequestContext/CefCookieManager never disposed at shutdown.
    The cached global instance outlived CefShutdown(), tripping
    DCHECK(all_.empty()) in browser_context.cc's exit-time leak detector
    (Debug builds only -- invisible in Release, since that check is compiled out
    there).
  4. CefClient.onGotFocus() synchronous infinite recursion. setFocus(true)
    synchronously re-fires OnWebContentsFocused -> onGotFocus() again for the
    same browser; CEF's own OnSetFocus() guards itself against this kind of
    reentrancy but OnGotFocus/OnWebContentsFocused has no equivalent guard. An
    already-focused browser recurses until the thread's stack overflows.
  5. ScopedJNIObject<T> mixed locked/unlocked accessors. A real SIGSEGV during
    ordinary handler-removal teardown, plus a related check-then-create race in
    GetOrCreateCefObject().
  6. Two browser-close hangs. OSR mode never fires OnBeforeClose for a browser
    whose renderer already died (e.g. after a deliberate chrome://crash);
    windowed mode had no fallback for the equivalent case either. Both now get a
    bounded fallback instead of hanging indefinitely.
  7. doMessageLoopWork's self-perpetuating Timer outliving native shutdown.
    An independently-scheduled Timer tick (up to ~33ms out) can still fire after
    N_Shutdown() has already destroyed the native Context singleton --
    harmless in Release, a real SIGSEGV in Debug builds where the resulting null-
    this DCHECK actually runs.
  8. CefClientHandler handler-removal methods used the abstract interface type
    instead of the concrete wrapper type.
    Follow-up to the ScopedJNIObject
    crash above.
  9. Two smaller fixes: a null-guard gap causing Debug-only DCHECK aborts in
    several _N.cpp setters (CefRequest/CefResponse/CefPostDataElement and
    others), and a CodeQL medium-severity finding (a world-readable temp file on
    POSIX).

Public API notes

Two new public static methods were added, both purely additive -- no existing
public signature was changed, removed, or had its contract broken:

  • CefRequestContext.disposeGlobalContext()
  • CefCookieManager.disposeGlobalManager()

Both are documented as internal-use-only (called from CefApp's shutdown sequence
to release the cached global instance before native CEF shutdown runs), but are
callable by any embedding app since they're public. Nothing else in the public
org.cef surface changed.

Credits

This work was done with substantial AI assistance (Claude, Anthropic) under my
direction and review -- disclosing that up front since it materially shaped how
this was produced.

Bug #5 above (ScopedJNIObject<T> mixed locked/unlocked accessors) was found by
diffing this project's teardown code against JetBrains/jcef (branch 261,
CEF 137), which independently added a locking mechanism around the same native-
pointer accessor call sites with a comment describing the identical crash
signature we hit. The fix here is adapted from that fork's approach.

Known limitation

The underlying browser_context.cc:44 DCHECK(all_.empty()) shutdown-time leak
detector is not fully fixed by #2/#3 above -- it still fires via at least one
further mechanism, confirmed independent of reference counting (ADDREF/RELEASE and
JNI ref counts both balance perfectly when it fires). The coverage CI job
recognizes this specific, well-understood signature and tolerates it rather than
treating it as a fresh failure, since it only ever fires after every test in a run
has already completed. Root-causing it further is still open.

Activity

  1. Thrameos commented on Sep 6, 2026

    @Thrameos
    Author

    Follow-up: more bugs found and fixed, plus 3 missing handler bindings

    Continuing to work through test coverage on the same fork. No action needed here either -- everything below is available to merge as-is, hand off to a volunteer, or cherry-pick individually. Each item is its own small, independently-reviewable PR on Thrameos/java-cef, linked below -- check the PR itself for current status (open, merged, or closed after merge).

    Bugs found and fixed

    1. Modified-UTF-8 (CESU-8) JNI string corruption. GetJNIString()/NewJNIString() used JNI's modified-UTF-8 string functions but treated the result as standard UTF-8, silently corrupting supplementary-plane characters (emoji, etc.) and embedded NULs crossing the JNI boundary in either direction. Thrameos/java-cef#1
    2. ColorType(int,int,int,int) sign-extends on high alpha. The packing constructor computed ARGB in 32-bit int arithmetic then relied on implicit widening to long; an alpha >= 0x80 set the sign bit, corrupting the upper 32 bits of color_value. Thrameos/java-cef#36
    3. OSR mouse wheel scroll direction is inverted. N_SendMouseWheelEvent passed AWT's wheel-rotation sign straight through; AWT's convention is the opposite of CefMouseEvent's. Thrameos/java-cef#37
    4. CefQueryCallback.success() ignores persistent=true. A persistent query is documented to allow repeated success() calls, but the native ref was unconditionally cleared after the first call regardless of persistent. Thrameos/java-cef#38
    5. DCHECK(handle) crash in CefMessageRouter.cancelPending(null, null). A null router-handler argument (documented as valid) still reached a constructor that DCHECKs non-null. Thrameos/java-cef#39
    6. GetCefValueFromJNIMap drops all data. Built a populated CefDictionaryValue but returned a brand-new empty CefValue instead of attaching it -- every Map passed to CefRequestContext.setPreference() silently lost its contents. Thrameos/java-cef#40
    7. setWindowVisibility() is a no-op for OSR/windowless browsers. Gated on OS_MACOSX and windowed mode only; should call CefBrowserHost::WasHidden() for windowless browsers instead. Found by comparing against a sibling fork (DanielTM999/java-cef, commit 6bd0cca1). Thrameos/java-cef#41
    8. Two more double-release causes in CefClient.cleanupBrowser() (ConcurrentModificationException-prone iteration + re-entrant handler removal) and RenderHandler::GetJNIScreenInfo() (double DeleteLocalRef) -- follow-up to issue Mac: Right-click context menu does not display in OSR mode #22. Thrameos/java-cef#42
    9. Malformed JNI input reaches CEF's internal CHECKs. N_SetToBytes didn't validate a negative/oversized size before an unsigned cast; N_SetHeaderByName/N_SetURL guarded null but not empty strings, and empty still trips CHECK(!x.empty()) (aborts Debug builds, silently wrong in Release). Thrameos/java-cef#43, plus the remaining call sites of the same empty-string gap in Thrameos/java-cef#49
    10. CefDownloadHandler.onBeforeDownload returning false can crash the process. Under Chrome-style runtime, false falls through to Chrome's own download-shelf default handling, which this embedding never implemented; if the browser is torn down before that deferred handling runs, CEF's ChooseDownloadPath crashes on an already-gone callback. Fixed by normalizing false to a safe cancel (drop the callback un-run) at the JNI boundary instead. Thrameos/java-cef#48

    Missing API surface added

    1. CefDevToolsMessageObserver was missing onDevToolsAgentAttached/onDevToolsAgentDetached, present on the real C++ interface. Thrameos/java-cef#50
    2. No binding for CefFindHandler (in-page find result callbacks) at all. Thrameos/java-cef#51
    3. No binding for CefFrameHandler (frame lifecycle: created/destroyed/attached/detached, main-frame-changed) at all. Thrameos/java-cef#52
    4. No binding for CefPermissionHandler (media access / permission-prompt requests) at all -- previously no way for a JCEF app to grant camera/mic/geolocation access; Alloy style silently denied everything. Thrameos/java-cef#53

    As before: substantial AI assistance (Claude, Anthropic) under my direction and review, disclosed up front.

  2. magreenblatt commented on Sep 6, 2026

    @magreenblatt
    Collaborator

    If you submit your changes as separate PRs (one PR per bug fix or enhancement, following the rules), they can be reviewed and potentially merged.

  3. Thrameos commented on Sep 6, 2026

    @Thrameos
    Author

    Do you have a preference on order?

    The coverage one is unfortunately massive because to add unittesting I had to fix the 10 bugs at once (too many segfaults to split them out safely and clear the CI). My original intent was a CI only PR which had no API changes at all and any problematic test was disabled. This issue being that the race conditions means that CI result is like a box of chocolates so many fixes had to be backported or it would never clear. The good news being almost all of it is in junit directory except the fixes. If you get that one in then all the others get much easier to deal with as you will get a green light from the CI not just on my PR but on anyone's linux PR that runs cleanly under xvfb and icevm. If it isn't covered with unittests, you can easily reject it and a working unittest makes is much easier to see if bug was an ordering problem with API usage.

    Alternatively, I can try to submit the 10 as single issue PR without the CI first, then the CI which with be no core changes (still huge but no API changes at once). But there is no guarantee the intermediate product works properly as testing each combination is rather costly.

    The other 14 are all small two to three file alterations that can be easily reviewed. But same issue applies that they are built on the 10 segfault fixes. Each has an individual unittest that clears the CI.

    I am not sure flooding you with 25 PRs at once would be very helpful. If you can specify your preferred method (and order) I can start the PR next weekend.

  4. magreenblatt commented on Sep 6, 2026

    @magreenblatt
    Collaborator

    Individual bug fixes and (non-test-support) enhancements should be proposed as separate PRs, with associated test/regression coverage. If that test/regression coverage depends on new test infrastructure, that infrastructure can be proposed first as a separate PR. The primary point being: don't mix unrelated concerns in a single PR. It's fine if that ends up being 25 (properly scoped) PRs.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions