Skip to content

fix(v3/linux): open the asset response pipe with O_CLOEXEC - #6216

Closed
DevLumuz wants to merge 1 commit into
wailsapp:masterfrom
DevLumuz:fix/linux-response-pipe-cloexec
Closed

DevLumuz wants to merge 1 commit into
wailsapp:masterfrom
DevLumuz:fix/linux-response-pipe-cloexec

Conversation

@DevLumuz

@DevLumuz DevLumuz commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Description

On Linux, the asset server's response writers (responsewriter_linux.go for GTK4, responsewriter_linux_gtk3.go for GTK3) stream the response body to WebKit through a pipe created with syscall.Pipe2(p, 0). Because the pipe has no O_CLOEXEC, any child process forked while a response is in flight inherits the write end. This includes os/exec, a helper the app restarts, and so on. The handler returns and closes its own copy, but WebKit only sees EOF on the body once every copy of the write end is closed. So the body, and the fetch() behind a binding call, stalls until that unrelated child exits, even though the response headers arrived on time.

In a real app this showed up as a binding call (GetMessages) whose result never reached JS. The Go method had returned in ~60 ms, but the promise resolved 9–17 s later, exactly when the app killed a helper process it had (re)started at the same moment. Instrumenting /proc confirmed it: during every stall, the helper held the write end of the response pipe, and the body completed the instant the helper died.

This PR creates the pipe with O_CLOEXEC on both ends. The read end is only consumed in-process by the GUnixInputStream, so nothing relies on it being inherited. The original comment ("we especially don't want to have the FD_CLOEXEC") gave no reason, and I could not find one. It is replaced by an explanation of why the flag is required.

No existing issue; happy to open one if preferred.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

Unit test: TestPipeIsCloseOnExec checks FD_CLOEXEC on both ends of pipe(). It fails on master and passes with this change, on both the default (GTK4) and -tags gtk3 builds. go test -race ./internal/assetserver/... passes on both builds.

End-to-end repro (minimal app, no frontend framework): a bound method returns an N KiB string. While it returns, a goroutine starts a few short-lived children, simulating an app that spawns helpers concurrently:

func (Repro) Big(kb int) string {
	go func() {
		for i := 0; i < 15; i++ {
			_ = exec.Command("sleep", "3").Start()
			time.Sleep(time.Millisecond)
		}
	}()
	return strings.Repeat("x", kb*1024)
}

The page calls Call.ByName("main.Repro.Big", kb) with an idle gap between calls and measures the time until the promise resolves. 6 calls per size (1, 16, 64, 256, 512, 1024 KiB), 36 calls per run:

Build (v3.0.0-beta.27) Stalled calls, master Stalled calls, this PR
GTK4 (default) 32 / 36, each ≈ 3003–3026 ms (= child lifetime) 0 / 36 (2–25 ms)
GTK3 (-tags gtk3) 30 / 36, each ≈ 3003–3026 ms 0 / 36 (3–29 ms)

Without the concurrent exec, both builds show 0 stalls. That is expected, since the bug needs a fork while the pipe is open.

  • Windows
  • macOS
  • Linux (Fedora 43)

Test Configuration

Fedora Linux 43, amd64, AMD Ryzen 5 4600H, NVIDIA GA107M
Go 1.25.4 / 1.26.7
gtk3 3.24.52, gtk4 4.20.4
webkit2gtk-4.1 2.52.5, webkitgtk-6.0 2.52.5
Wails v3.0.0-beta.27 (master @ 90486a938)

Checklist:

  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • Bug Fixes
    • Improved process isolation on Linux by ensuring communication resources are not inherited by child processes. This reduces the risk of unintended resource use and supports more reliable process behavior.

The Linux response writers stream the body to WebKit through a pipe created
with Pipe2(p, 0). Without O_CLOEXEC, any child process forked while a
response is in flight (os/exec, a helper the app spawns) inherits the write
end. The handler returns and closes its copy, but WebKit only sees EOF once
every copy is closed, so the body (and the fetch() behind a binding call)
stalls until that child exits, even though headers arrived on time.

Create the pipe with O_CLOEXEC on both ends. The read end is only consumed
in-process by the GUnixInputStream, so nothing relies on inheriting it.
Applies to both the GTK4 and GTK3 writers.
@github-actions github-actions Bot added Bug Something isn't working v3 labels Oct 3, 2026
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: wailsapp/wails/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d74c79a4-827c-49dc-a36c-3743de2d4238
📥 Commits

Reviewing files that changed from the base of the PR and between 90486a9 and 0c6eda3.

📒 Files selected for processing (3)
  • v3/internal/assetserver/webview/responsewriter_linux.go
  • v3/internal/assetserver/webview/responsewriter_linux_gtk3.go
  • v3/internal/assetserver/webview/responsewriter_linux_test.go

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

Both Linux response pipe implementations now create descriptors with close-on-exec enabled. A Linux test checks that both descriptors have the FD_CLOEXEC flag.

Changes

Linux response pipe descriptors

Layer / File(s) Summary
Set and verify close-on-exec
v3/internal/assetserver/webview/responsewriter_linux.go, v3/internal/assetserver/webview/responsewriter_linux_gtk3.go, v3/internal/assetserver/webview/responsewriter_linux_test.go
Both implementations pass O_CLOEXEC when creating the pipe. The GTK3 comment opposing FD_CLOEXEC is removed. The test checks that both pipe descriptors have FD_CLOEXEC set.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: leaanthony

Merge Risk: ⚪ Minimal · up to 0c6ed

The change stops child processes from inheriting the response pipe, which removes the reported stalls in response bodies. No merge-blocking risk was found.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 0c6ed

The change prevents unintended response-pipe access after child processes execute another program, while preserving the existing response delivery and cleanup behavior. No introduced or worsened security concern was identified in the reviewed change.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed authority is access to an application's Linux WebView response-pipe descriptors after exec. An otherwise inherited read end could permit consuming response bytes, and an inherited write end could permit writes or delay EOF. The change removes this implicit access across exec rather than expanding it.

Trust Boundaries and Controls

  • observed — The control is installed on both pipe ends at creation, while the existing WebKit consumer receives the read descriptor directly within the application process. That handoff does not depend on the descriptor surviving exec.

Resilience and Maintainability Implications

  • observed — Pipe creation errors follow the existing response-error path without installing a writer. Handoff failure closes an installed writer, and Finish guards repeated completion. The flag change introduces no retry or recovery transition; existing interruption handling and unsynchronized lifecycle behavior are not newly introduced by this PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the Linux asset response pipe change and its use of O_CLOEXEC.
Description check ✅ Passed The description explains the bug, fix, motivation, and testing in detail. It identifies the change as a bug fix, reports Linux test configuration, and completes the relevant test checkboxes. Some chec…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks each pipe end bright,
Both close on exec, as flags say right.
The WebKit stream can see its end,
No child keeps the pipe to lend.
I hop away; the test will mend!

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the Linux label Oct 3, 2026
@DevLumuz DevLumuz closed this by deleting the head repository Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working Linux v3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant