Skip to content

Campfire pin: what breaks after Page < Array when moving to basecamp/main (CSRF :header_only, RichText#embeds, Lexxy's input) #514

Description

@namespaceMarcello

Moving CAMPFIRE_SHA from 90b3300 to current basecamp/once-campfire main stops first at Spinel rejecting class Page < Array (app/models/message/pagination.rb:8). That one is already tracked in matz/spinel#7449, matz/spinel#7584 and #472. To see what comes after it, we built Campfire main with the Spinel from matz/spinel#7584 and ran three of the Campfire gates from ci.yml locally. One gate fails for two reasons (the second shows once the first is patched around), and one test file goes red. We couldn't find an issue or PR for any of them (we searched issues and PRs for Sec-Fetch-Site, header_only, CSRF, authenticity, embeds, lexxy, editor_adapter).

Is anyone already on these, for example as part of the Campfire preview work in #462? If not, we'd be glad to send one PR per item.

Setup

scripts/build-campfire-archive --out out /path/to/campfire   # spinel from matz/spinel#7584 on PATH
scripts/campfire-oracle prepare --app /path/to/campfire
scripts/campfire-compare --spinel /path/to/campfire
scripts/campfire-db-differential --spinel /path/to/campfire
scripts/campfire-suite /path/to/campfire

The archive builds: spin pack ok, the C compiles with no errors, and the 13 MB binary starts (/first_run 200, /up 200). campfire-db-differential --spinel is green: 0 tables differ, rollback 17/17.

1. campfire-compare --spinel: the Rails oracle accepts the forged posts

The Rails walk fails before the emit is compared:

==> a forged post, which must be refused
  FAIL a POST without the token is refused: got "200", want "422"
  ok   the right token from a foreign Origin is refused
  FAIL neither reaches a socket: got true, want false
  FAIL a planted session cookie's token is refused: got "200", want "422"

Cause. With load_defaults 8.2, today's Rails defaults to forgery_protection_verification_strategy = :header_only (rails/rails#56350). In action_controller/metal/request_forgery_protection.rb at e3d5c569, verified_via_header_only? returns true for same-origin and same-site, origin_trusted? for cross-site, and, when the header is absent, true unless the request is SSL or the app forces a secure protocol. The probes in scripts/campfire-cable-drive.rb (around lines 410-437) send no Sec-Fetch-Site over plain http, so Rails lets them through. The Rails at the current pin checked the token.

Check. We sent Sec-Fetch-Site: cross-site on the two forged posts and same-site on the planted one:

-             accept: "text/vnd.turbo-stream.html, text/html", csrf: false)
+             accept: "text/vnd.turbo-stream.html, text/html", csrf: false, headers: { "Sec-Fetch-Site" => "cross-site" })
 ...
-              accept: "text/vnd.turbo-stream.html, text/html", origin: "https://attacker.example")
+              accept: "text/vnd.turbo-stream.html, text/html", origin: "https://attacker.example", headers: { "Sec-Fetch-Site" => "cross-site" })
 ...
-              accept: "text/vnd.turbo-stream.html, text/html", token: PLANTED)
+              accept: "text/vnd.turbo-stream.html, text/html", token: PLANTED, headers: { "Sec-Fetch-Site" => "same-site" })

Rails then returns 422 and 422, and nothing reaches a socket. The planted-cookie post still gets 200, which matches the code above: :header_only accepts same-site without looking at the token.

Open question. The runtime checks the masked token: verified_request? in runtime/ruby/action_controller/base.rb:603, and the Spinel reopening in runtime/spinel/request_forgery_protection.rb:10-34, which adds the Origin check. Should the probes only gain the header, or should the runtime follow verified_via_header_only? so the two lanes keep agreeing? We haven't measured how the emit answers the planted same-site post: the Rails walk stops first, and for item 2 we removed that probe.

2. With the probes patched, room.html differs by one attribute

With the header patch above and the planted probe removed, both walks are green. frame_text.html and frame_html.html are equivalent. room.html differs only in the composer: the emit's <lexxy-editor> carries input="message_body_trix_input_message", and Rails' doesn't. All the other attributes match.

Cause. Lexxy is 0.9.24 both at the pin and today. At boot it picks a path with Lexxy.supports_editor_adapter? (lib/lexxy.rb:6), which is true when ActionText::Editor#editor_tag takes a block (rails/rails#56926):

  • the pin's Rails doesn't have that, so lib/lexxy/engine.rb loads the fallback helpers, and lib/lexxy/action_text_tag.rb:13 sets options["input"];
  • today's Rails has it, so Lexxy registers ActionText::Editor::LexxyEditor and doesn't load the fallback.

The emitter reproduces the fallback: src/lower/view_to_library/form_builder.rs:1790-1799 says "the path Rails without ActionText::Editor takes, campfire's".

3. campfire-suite: RichText#embeds is missing

FAIL test/models/message_test  6/8
     FAIL MessageTest#test_presentation_associations_load_together: undefined method 'embeds' for an instance of ActionText::RichText
==> 416 of 418 tests pass (69 of 70 files green), 2 skipped

That is still above the floor in ci.yml (392 tests, 66 files), so the conformance job would stay green. The test comes from once-campfire#292 (659f957, 2026-10-04). It calls message.body.embeds.each(&:filename) inside assert_no_queries, after with_presentation → with_attachment_details → with_rich_text_body_and_embeds. Roundhouse already types that scope (src/analyze/mod.rs:560-569), but the ActionText::RichText model it synthesizes (src/lower/rich_text.rs) has no embeds attachments. We haven't measured whether the preload would then satisfy assert_no_queries.

Not run

  • campfire-compare and campfire-db-differential on the ruby target
  • campfire-compare --spinel with --minor-gc and --verify-gen
  • the Spinel suite lane, the smoke jobs and the benches

Written by Claude Code (AI assistant) on behalf of @namespaceMarcello, who directs this work.

Activity

  1. namespaceMarcello commented on Oct 7, 2026

    @namespaceMarcello
    ContributorAuthor

    An update now that matz/spinel#7584 has merged (2026-10-07 00:03Z).

    Built locally from roundhouse 607bb24f, Spinel master f3da0151f and Campfire basecamp/main 8a6e429: the archive and the binary build (25 MB of generated C). The CI gates on that tree:

    • campfire-db-differential --spinel: green, rollback 17/17.
    • campfire-compare --spinel: stops on the Rails walk at the same three CSRF probes described above (:header_only; the probes send no Sec-Fetch-Site). The Lexxy input difference comes after that point, so this run didn't reach it.
    • campfire-suite (ruby): 412/419 tests, 66/70 files. Apart from RichText#embeds, two new failures come from 8a6e429, Campfire's fix for GHSA-3v99-4vxh-xg84 (message DOM ids):
      • helpers.dom_id(...) in app/controllers/messages/boosts_controller.rb:39. Roundhouse rewrites ActionController::Base.helpers (is_base_dot_helpers in src/emit/ruby/library.rs), but not helpers called inside an action, so 4 boost tests fail with undefined local variable or method 'helpers'.
      • css_select in test/controllers/messages_controller_test.rb:160 is not in the test runtime (1 test).

    That also means the binary built at the current pin doesn't include that security fix.

    What moving the pin gains, measured on #496's larger database (2M messages, room 1 with 1M). Same roundhouse and Spinel for both binaries, 4 CPUs (AMD Ryzen 9 7940HX, Docker Desktop), one request at a time, median of 11:

    binary at the pin 90b3300 binary at basecamp/main 8a6e429
    room page, 1M messages 435 ms 9.7 ms
    messages?before= mid-room 287 ms 4.3 ms
    search 86 ms 14 ms

    Most of the gain comes from the (room_id, created_at) index and the paging and search changes Campfire merged on 2026-10-05 (basecamp/once-campfire#295, #297, #304).

    So the open list for the pin is the three points above plus helpers in controllers and css_select. Is anyone already working on any of them, for example in #462's preview work? If not, I can send one PR per point. For CSRF and Lexxy the question above still stands (probes only, or a runtime that follows :header_only and the ActionText::Editor form): the pin's Rails and today's both report 8.2.0.alpha, so the lockfile can't tell them apart.

    Unrelated to the pin: #535 fixes the sidebar slowdown in #496.

    Written by Claude Code (AI assistant) on behalf of @namespaceMarcello, who directs this work.

  2. thomasklemm commented on Oct 9, 2026

    @thomasklemm
    Collaborator

    Thanks for the detailed write-up. We are picking these up now, so no need to send separate PRs.


    Generated by Claude Code

  3. namespaceMarcello commented on Oct 9, 2026

    @namespaceMarcello
    ContributorAuthor

    Happy to help

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