Skip to content

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

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

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

Conversation

@DevLumuz

@DevLumuz DevLumuz commented Oct 7, 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.

Note: Supersedes #6216 which was accidentally closed due to a deleted fork.

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.28

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-launch reliability on Linux by preventing internal communication channels from being inherited by child processes. This helps avoid unintended interference and lingering resources when launching applications.

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 Linux labels Oct 7, 2026
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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: 152102f9-b01f-4980-834a-23e760003f94
📥 Commits

Reviewing files that changed from the base of the PR and between dff193f and ce2cdae.

📒 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 set close-on-exec. A Linux test checks that both pipe descriptors have the flag set.

Changes

Linux response pipe

Layer / File(s) Summary
Set and verify close-on-exec flags
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 to Pipe2. The GTK3 comment describes how inherited write ends can delay EOF. The test checks FD_CLOEXEC on both pipe descriptors.

Priority: ⬆️ High

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

Change: Bug fix

Suggested reviewers: leaanthony

Merge Risk: ⚪ Minimal · up to ce2cd

The change prevents child processes started through exec from keeping response pipes open after the writer finishes. The inspected GTK response flow remains intact, so no merge-blocking concern remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the Linux pipe change and the use of O_CLOEXEC.
Description check ✅ Passed The description provides detailed motivation, bug impact, implementation scope, issue context, testing results, Linux test configuration, and completed relevant checklist items. It does not explicitly…
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 with care,
Two close-on-exec flags now are there.
The test looks at both ends,
And checks the flag that each one sends.
Then off I hop through evening air.

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

@taliesin-ai

Copy link
Copy Markdown
Collaborator

Automated v3 GA-readiness test result for head ce2cdae35240185fdf47f69383e139a57a0c148f

Platform CLI build Unit tests (./pkg/... ./internal/...) Example build (examples/window)
macOS (mac-node1) PASS PASS (51/51 pkgs) PASS
Windows (win-node1) PASS PASS (52/52 pkgs) PASS
Linux not run (Linux node unavailable)

This change is Linux-only, so the code it touches (the asset pipe O_CLOEXEC path) was not exercised by this run. The run only confirms that the PR does not break macOS or Windows builds and tests, and those results match the master baseline at 1a6053d8. The app was not launched because there was no GUI session over SSH. This comment is only a build and test report. It is not a code review.

This branch has not been deployed

No deployments
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.

2 participants