detached 7fae873

fix(runtime): prevent stack shrinking across CEF callbacks

commit
7fae873ff2395c85f60984e63cb983f5013afc64
author
brice · · 2026-07-27 02:58 UTC
parent
3eb9180
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +97 -0
M cmd/dumber/main.go +71 -0
@@ -146,7 +146,78 @@ type startupTiming struct {
146146 configComplete time.Time
147147 }
148148
149+func environmentValue(environ []string, name string) string {
150+ prefix := name + "="
151+ for _, entry := range environ {
152+ if strings.HasPrefix(entry, prefix) {
153+ return strings.TrimPrefix(entry, prefix)
154+ }
155+ }
156+ return ""
157+}
158+
159+func replaceEnvironmentValue(environ []string, name, value string) []string {
160+ prefix := name + "="
161+ updated := make([]string, 0, len(environ)+1)
162+ for _, entry := range environ {
163+ if !strings.HasPrefix(entry, prefix) {
164+ updated = append(updated, entry)
165+ }
166+ }
167+ return append(updated, prefix+value)
168+}
169+
170+func runtimeSafetyEnvironment(environ []string) ([]string, bool) {
171+ updated := append([]string(nil), environ...)
172+ const setting = "gcshrinkstackoff"
173+ parts := strings.Split(environmentValue(updated, "GODEBUG"), ",")
174+ kept := make([]string, 0, len(parts)+1)
175+ alreadySafe := false
176+ for _, part := range parts {
177+ part = strings.TrimSpace(part)
178+ if part == "" {
179+ continue
180+ }
181+ key, value, found := strings.Cut(part, "=")
182+ if key == setting {
183+ alreadySafe = found && value == "1"
184+ continue
185+ }
186+ kept = append(kept, part)
187+ }
188+ if alreadySafe {
189+ return updated, false
190+ }
191+ kept = append(kept, setting+"=1")
192+ updated = replaceEnvironmentValue(updated, "GODEBUG", strings.Join(kept, ","))
193+ return updated, true
194+}
195+
196+func ensureRuntimeSafety() error {
197+ environ, reexec := runtimeSafetyEnvironment(os.Environ())
198+ if !reexec {
199+ return nil
200+ }
201+ executable, err := os.Executable()
202+ if err != nil {
203+ return fmt.Errorf("resolve executable for runtime safety re-exec: %w", err)
204+ }
205+ if err := syscall.Exec(executable, os.Args, environ); err != nil {
206+ return fmt.Errorf("runtime safety re-exec: %w", err)
207+ }
208+ return nil
209+}
210+
149211 func main() {
212+ // purego callbacks cross foreign CEF/GTK stacks. Go stack shrinking can
213+ // invalidate frames retained across those boundaries and make the GC abort
214+ // with "traceback did not unwind completely". Re-exec once so the runtime
215+ // sees the safety setting before it creates any goroutine or CEF subprocess.
216+ if err := ensureRuntimeSafety(); err != nil {
217+ _, _ = fmt.Fprintf(os.Stderr, "dumber: %v\n", err)
218+ os.Exit(1)
219+ }
220+
150221 // CEF is not known until configuration is complete. Keep these neutral
151222 // timestamps so a CEF GUI trace can be seeded truthfully without creating
152223 // one for WebKit, the standalone omnibox, CLI, or CEF helper processes.
@@ -146,7 +146,78 @@ type startupTiming struct {
146 configComplete time.Time146 configComplete time.Time
147 }147 }
148 148
149+func environmentValue(environ []string, name string) string {
150+ prefix := name + "="
151+ for _, entry := range environ {
152+ if strings.HasPrefix(entry, prefix) {
153+ return strings.TrimPrefix(entry, prefix)
154+ }
155+ }
156+ return ""
157+}
158+
159+func replaceEnvironmentValue(environ []string, name, value string) []string {
160+ prefix := name + "="
161+ updated := make([]string, 0, len(environ)+1)
162+ for _, entry := range environ {
163+ if !strings.HasPrefix(entry, prefix) {
164+ updated = append(updated, entry)
165+ }
166+ }
167+ return append(updated, prefix+value)
168+}
169+
170+func runtimeSafetyEnvironment(environ []string) ([]string, bool) {
171+ updated := append([]string(nil), environ...)
172+ const setting = "gcshrinkstackoff"
173+ parts := strings.Split(environmentValue(updated, "GODEBUG"), ",")
174+ kept := make([]string, 0, len(parts)+1)
175+ alreadySafe := false
176+ for _, part := range parts {
177+ part = strings.TrimSpace(part)
178+ if part == "" {
179+ continue
180+ }
181+ key, value, found := strings.Cut(part, "=")
182+ if key == setting {
183+ alreadySafe = found && value == "1"
184+ continue
185+ }
186+ kept = append(kept, part)
187+ }
188+ if alreadySafe {
189+ return updated, false
190+ }
191+ kept = append(kept, setting+"=1")
192+ updated = replaceEnvironmentValue(updated, "GODEBUG", strings.Join(kept, ","))
193+ return updated, true
194+}
195+
196+func ensureRuntimeSafety() error {
197+ environ, reexec := runtimeSafetyEnvironment(os.Environ())
198+ if !reexec {
199+ return nil
200+ }
201+ executable, err := os.Executable()
202+ if err != nil {
203+ return fmt.Errorf("resolve executable for runtime safety re-exec: %w", err)
204+ }
205+ if err := syscall.Exec(executable, os.Args, environ); err != nil {
206+ return fmt.Errorf("runtime safety re-exec: %w", err)
207+ }
208+ return nil
209+}
210+
149 func main() {211 func main() {
212+ // purego callbacks cross foreign CEF/GTK stacks. Go stack shrinking can
213+ // invalidate frames retained across those boundaries and make the GC abort
214+ // with "traceback did not unwind completely". Re-exec once so the runtime
215+ // sees the safety setting before it creates any goroutine or CEF subprocess.
216+ if err := ensureRuntimeSafety(); err != nil {
217+ _, _ = fmt.Fprintf(os.Stderr, "dumber: %v\n", err)
218+ os.Exit(1)
219+ }
220+
150 // CEF is not known until configuration is complete. Keep these neutral221 // CEF is not known until configuration is complete. Keep these neutral
151 // timestamps so a CEF GUI trace can be seeded truthfully without creating222 // timestamps so a CEF GUI trace can be seeded truthfully without creating
152 // one for WebKit, the standalone omnibox, CLI, or CEF helper processes.223 // one for WebKit, the standalone omnibox, CLI, or CEF helper processes.
M cmd/dumber/main_test.go +26 -0
@@ -14,6 +14,32 @@ import (
1414 "github.com/rs/zerolog"
1515 )
1616
17+func TestRuntimeSafetyEnvironmentDisablesStackShrinkingBeforeStartup(t *testing.T) {
18+ tests := []struct {
19+ name string
20+ environ []string
21+ wantGODEBUG string
22+ wantReexec bool
23+ }{
24+ {name: "missing", environ: []string{"HOME=/tmp"}, wantGODEBUG: "gcshrinkstackoff=1", wantReexec: true},
25+ {name: "preserves settings", environ: []string{"GODEBUG=gctrace=1"}, wantGODEBUG: "gctrace=1,gcshrinkstackoff=1", wantReexec: true},
26+ {name: "overrides unsafe value", environ: []string{"GODEBUG=gcshrinkstackoff=0,gctrace=1"}, wantGODEBUG: "gctrace=1,gcshrinkstackoff=1", wantReexec: true},
27+ {name: "already safe", environ: []string{"GODEBUG=gctrace=1,gcshrinkstackoff=1"}, wantGODEBUG: "gctrace=1,gcshrinkstackoff=1", wantReexec: false},
28+ {name: "forged marker is ignored", environ: []string{"DUMBER_RUNTIME_SAFETY_REEXEC=1", "GODEBUG=gcshrinkstackoff=0"}, wantGODEBUG: "gcshrinkstackoff=1", wantReexec: true},
29+ }
30+ for _, tt := range tests {
31+ t.Run(tt.name, func(t *testing.T) {
32+ got, reexec := runtimeSafetyEnvironment(tt.environ)
33+ if reexec != tt.wantReexec {
34+ t.Fatalf("reexec = %v, want %v", reexec, tt.wantReexec)
35+ }
36+ if value := environmentValue(got, "GODEBUG"); value != tt.wantGODEBUG {
37+ t.Fatalf("GODEBUG = %q, want %q", value, tt.wantGODEBUG)
38+ }
39+ })
40+ }
41+}
42+
1743 func TestLaunchModeFromArgs_DetectsStandaloneOmnibox(t *testing.T) {
1844 mode, _ := launchModeFromArgs([]string{"dumber", "omnibox"})
1945 if mode != launchModeStandaloneOmnibox {
@@ -14,6 +14,32 @@ import (
14 "github.com/rs/zerolog"14 "github.com/rs/zerolog"
15 )15 )
16 16
17+func TestRuntimeSafetyEnvironmentDisablesStackShrinkingBeforeStartup(t *testing.T) {
18+ tests := []struct {
19+ name string
20+ environ []string
21+ wantGODEBUG string
22+ wantReexec bool
23+ }{
24+ {name: "missing", environ: []string{"HOME=/tmp"}, wantGODEBUG: "gcshrinkstackoff=1", wantReexec: true},
25+ {name: "preserves settings", environ: []string{"GODEBUG=gctrace=1"}, wantGODEBUG: "gctrace=1,gcshrinkstackoff=1", wantReexec: true},
26+ {name: "overrides unsafe value", environ: []string{"GODEBUG=gcshrinkstackoff=0,gctrace=1"}, wantGODEBUG: "gctrace=1,gcshrinkstackoff=1", wantReexec: true},
27+ {name: "already safe", environ: []string{"GODEBUG=gctrace=1,gcshrinkstackoff=1"}, wantGODEBUG: "gctrace=1,gcshrinkstackoff=1", wantReexec: false},
28+ {name: "forged marker is ignored", environ: []string{"DUMBER_RUNTIME_SAFETY_REEXEC=1", "GODEBUG=gcshrinkstackoff=0"}, wantGODEBUG: "gcshrinkstackoff=1", wantReexec: true},
29+ }
30+ for _, tt := range tests {
31+ t.Run(tt.name, func(t *testing.T) {
32+ got, reexec := runtimeSafetyEnvironment(tt.environ)
33+ if reexec != tt.wantReexec {
34+ t.Fatalf("reexec = %v, want %v", reexec, tt.wantReexec)
35+ }
36+ if value := environmentValue(got, "GODEBUG"); value != tt.wantGODEBUG {
37+ t.Fatalf("GODEBUG = %q, want %q", value, tt.wantGODEBUG)
38+ }
39+ })
40+ }
41+}
42+
17 func TestLaunchModeFromArgs_DetectsStandaloneOmnibox(t *testing.T) {43 func TestLaunchModeFromArgs_DetectsStandaloneOmnibox(t *testing.T) {
18 mode, _ := launchModeFromArgs([]string{"dumber", "omnibox"})44 mode, _ := launchModeFromArgs([]string{"dumber", "omnibox"})
19 if mode != launchModeStandaloneOmnibox {45 if mode != launchModeStandaloneOmnibox {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close