Skip to content

Conversation

@luoliwoshang
Copy link
Member

@luoliwoshang luoliwoshang commented Nov 11, 2025

Issue #1389 Root Cause Analysis Report

Problem Overview

When importing the go/build package (an overlay patch package), the program crashes at runtime with the error:

panic: runtime error: invalid memory address or nil pointer dereference

The crash occurs during io/fs.init$after when initializing the runtime metadata for the io/fs.DirEntry interface type.


Crash Stack Analysis

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x14)
  * frame #0: 0x00000001002aed88 t`github.com/goplus/llgo/runtime/abi.(*Type).String + 12
    frame #1: 0x00000001002b53c4 t`github.com/goplus/llgo/runtime/internal/runtime.interfaceStr + 716
    frame #2: 0x00000001002c3fc0 t`github.com/goplus/llgo/runtime/internal/runtime.Interface + 236
    frame #3: 0x00000001000f2fd0 t`io/fs.init$after + 23396
    frame #4: 0x00000001000ec624 t`io/fs.init + 44
    frame #5: 0x000000010012e8cc t`os.init + 48
    frame #6: 0x0000000100153f04 t`fmt.init + 72
    frame #7: 0x00000001002a0d9c t`go/build.init$hasPatch + 80
    frame #8: 0x00000001002a086c t`go/build.init + 48
    frame #9: 0x00000001002aded0 t`command-line-arguments.init + 44
    frame #10: 0x00000001002cb21c t`main + 32

Key Points:

  • Crash occurs during io/fs.init$after execution
  • Call chain: Interface()interfaceStr()(*Type).String()
  • Access to address 0x14, which is a null pointer dereference when accessing the TFlag field (offset 0x14) of the Type struct

Runtime Debugging Findings

1. Added Debugging Code

Added debug output in runtime/internal/runtime/z_face.go:

func interfaceStr(ft *abi.InterfaceType) string {
    repr := make([]byte, 0, 64)
    repr = append(repr, "interface {"...)
    c.Printf(c.Str("interfaceStr.PkgPath: %s\n"), c.AllocCStr(ft.PkgPath_))

    for i, t := range ft.Methods {
        if i > 0 {
            repr = append(repr, ';')
        }
        c.Printf(c.Str(" t.Name (%s)\n"), c.AllocCStr(t.Name_))
        repr = append(repr, ' ')
        repr = append(repr, t.Name_...)
        repr = append(repr, t.Typ_.String()[4:]...)  // ← Crash here!
        c.Printf(c.Str("\n"))
    }
    // ...
}

func (t *Type) String() string {
    if t == nil {
        c.Printf(c.Str(" (*Type).String: t is nil!\n"))
        return "<nil Type>"
    }
    c.Printf(c.Str("Type.String: %s (kind=%d)\n"), c.AllocCStr(t.Str_), t.Kind_)
    // ...
}

2. Debug Output

Interface: rtypeList.findInterface(pkgPath, methods) io/fs 0 Info Info; 1 IsDir IsDir; 2 Name Name; 3 Type Type;
interfaceStr.PkgPath: io/fs
 t.Name (Info)
Type.String: func() (fs.FileInfo, error) (kind=51)

 t.Name (IsDir)
Type.String: func() bool (kind=51)

 t.Name (Name)
Type.String: func() string (kind=51)

 t.Name (Type)
 (*Type).String: t is nil!

Key Finding:

  • io/fs.DirEntry interface has 4 methods: Info(), IsDir(), Name(), Type()
  • The first 3 methods have normal function types
  • The 4th method Type() has a nil abi.Imethod.Typ_ field

3. DirEntry Interface Definition

type DirEntry interface {
    Name() string
    IsDir() bool
    Type() FileMode      // ← This method's type is missing!
    Info() (FileInfo, error)
}

The Type() method has signature func() FileMode, but this function type was not properly initialized.


Overlay Comparison Experiment

Experiment Method

Remove go/build from hasAltPkg in runtime/build.go:

var hasAltPkg = map[string]none{
    // "go/build": {},  // ← Comment out this line
    "math":     {},
    "os":       {},
    // ...
}

Experiment Result

After removing overlay: The program runs normally!

This proves the issue is related to the go/build overlay patch mechanism.


Initialization Order Comparison

1. Correct Initialization Order Without Overlay

_llgo_1:                                          ; preds = %_llgo_0
  store i1 true, ptr @"go/build.init$guard", align 1
  call void @bytes.init()
  call void @errors.init()
  call void @fmt.init()
  call void @"go/ast.init"()
  call void @"go/build/constraint.init"()
  call void @"go/doc.init"()
  call void @"go/token.init"()
  call void @"internal/buildcfg.init"()
  call void @"internal/godebug.init"()
  call void @"internal/goroot.init"()
  call void @"internal/goversion.init"()
  call void @"internal/platform.init"()
  call void @"internal/syslist.init"()
  call void @io.init()
  call void @"io/fs.init"()              // ← io/fs fully initialized here
  call void @os.init()
  call void @"os/exec.init"()
  call void @path.init()
  call void @"path/filepath.init"()
  call void @slices.init()
  call void @strconv.init()
  call void @strings.init()
  call void @unicode.init()
  call void @"unicode/utf8.init"()
  call void @bufio.init()
  call void @"go/parser.init"()
  call void @"go/scanner.init"()
  call void @"go/build.init$after"()    // ← init$after executed last

Order: Initialize all dependency packages → then execute package's init$after

2. Incorrect Initialization Order With Overlay

_llgo_1:                                          ; preds = %_llgo_0
  store i1 true, ptr @"go/build.init$guard", align 1
  call void @"go/build.init$after"()    // ← Execute init$after first!
  call void @"go/build.init$hasPatch"() // ← Then initialize dependencies via hasPatch
  br label %_llgo_2

Order: Execute init$after first → then initialize dependency packages via init$hasPatch

Problem:

  1. go/build.init$after uses the io/fs.FileMode type
  2. This causes io/fs.FileMode to be prematurely initialized (only creating type shell, incomplete)
  3. Subsequently go/build.init$hasPatch calls io/fs.init()io/fs.init$after
  4. At this point io/fs.FileMode is already non-null, triggering the compiler's incorrect logic

LLVM IR Code Analysis

1. The Only Initialization Location for func() FileMode Type

_llgo_121 Basic Block:

_llgo_121:                                        ; preds = %_llgo_119
  %1367 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 0)
  %1368 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %1367, 0
  %1369 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %1368, i64 0, 1
  %1370 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %1369, i64 0, 2
  %1371 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 8)
  %1372 = getelementptr ptr, ptr %1371, i64 0
  store ptr %1358, ptr %1372, align 8
  %1373 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %1371, 0
  %1374 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %1373, i64 1, 1
  %1375 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %1374, i64 1, 2
  store %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %1370, ptr %63, align 8
  store %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %1375, ptr %64, align 8
  %1376 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Func"(ptr %63, ptr %64, i1 false)
  call void @"github.com/goplus/llgo/runtime/internal/runtime.SetDirectIface"(ptr %1376)
  store ptr %1376, ptr @"_llgo_func$s8U8kwnQmuJamqzoKKCmURes5qoxCGgMrAgxIKBHdmE", align 8
  br label %_llgo_122

Key Points:

  • @"_llgo_func$s8U8kwnQmuJamqzoKKCmURes5qoxCGgMrAgxIKBHdmE" is the func() FileMode type
  • This is the only place in the entire IR file where this type is initialized
  • If this basic block is not executed, func() FileMode will never be initialized

2. Control Flow Analysis

Precondition for _llgo_121:

_llgo_118:                                        ; preds = %_llgo_117, %_llgo_116
  %1343 = load ptr, ptr @_llgo_uint32, align 8
  br i1 %443, label %_llgo_119, label %_llgo_120
  • Only when %443 == true will it jump to _llgo_119
  • _llgo_119 checks if func() FileMode is null, then jumps to _llgo_121 for initialization
  • If %443 == false, it jumps to _llgo_120, skipping the entire initialization block

3. Definition of %443

_llgo_46 Basic Block:

_llgo_46:                                         ; preds = %_llgo_114, %_llgo_38
  %439 = load ptr, ptr @_llgo_time.Time, align 8
  %440 = load ptr, ptr @"_llgo_func$1UWkRgkSfkoeR6IHk7YT7VuzZs_iYXeP7dCKXWWOCE8", align 8
  %441 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.NewNamed"(
      %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @0, i64 5 },
      %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @94, i64 8 },
      i64 10, i64 4, i64 5, i64 5)
  %442 = load ptr, ptr @"_llgo_io/fs.FileMode", align 8
  %443 = icmp eq ptr %442, null
  br i1 %443, label %_llgo_115, label %_llgo_116

Key Logic:

%442 = load ptr, ptr @"_llgo_io/fs.FileMode", align 8
%443 = icmp eq ptr %442, null
  • %443 represents whether @"_llgo_io/fs.FileMode" is null
  • Only when FileMode is null (%443 = true) will method type initialization be executed
  • If FileMode is not null (%443 = false), method type initialization is skipped

4. Control Flow Graph

_llgo_46:
  %442 = load @"_llgo_io/fs.FileMode"
  %443 = icmp eq %442, null
  br %443, _llgo_115, _llgo_116
         ↓                  ↓
    %443=true          %443=false
         ↓                  ↓
   _llgo_115          _llgo_116
         ↓                  ↓
         ... →  _llgo_118 ← ...
                     ↓
           br %443, _llgo_119, _llgo_120
                 ↓                  ↓
            %443=true          %443=false
                 ↓                  ↓
            _llgo_119          _llgo_120 (Skip initialization!)
                 ↓
            _llgo_121 (Initialize func() FileMode)

Core Problem:

  • The compiler computes %443 = whether FileMode is null in _llgo_46
  • In _llgo_118, it reuses the same %443 variable to control method type initialization
  • This causes method type initialization to be bound to the same condition as parent type creation

Root Cause Identification

IR Generation Error

The compiler has a logic error when generating LLVM IR for type initialization:

Incorrect Logic:

  1. First generate null check for parent type (e.g., FileMode): %443 = (FileMode == nil)
  2. Then reuse the same %443 variable to control method type initialization
  3. Result: Method type initialization is bound to the same condition as parent type creation

Generated IR Code:

; Null check for parent type
%442 = load ptr, ptr @"_llgo_io/fs.FileMode", align 8
%443 = icmp eq ptr %442, null
br i1 %443, label %create_parent, label %skip_parent

; ... Parent type creation ...

; Method type creation (incorrectly reuses %443)
br i1 %443, label %create_methods, label %skip_methods  ; ← Error!

Correct Logic Should Be:

  • Parent type has its own null check
  • Method type should also have an independent null check
  • The two should not share the same condition variable

Correct IR Should Be:

; Null check for parent type
%442 = load ptr, ptr @"_llgo_io/fs.FileMode", align 8
%443 = icmp eq ptr %442, null
br i1 %443, label %create_parent, label %skip_parent

; Independent null check for method type
%method_ptr = load ptr, ptr @"_llgo_func$...FileMode"
%method_null = icmp eq ptr %method_ptr, null
br i1 %method_null, label %create_method, label %skip_method

Two Necessary Conditions for the Bug

This bug requires two errors to exist simultaneously to be triggered:

Error 1: Incorrect Init Order

Problem: Overlay package's init$after executes before init$hasPatch

Consequence:

  • go/build.init$after executes prematurely
  • Uses io/fs.FileMode, causing it to be prematurely initialized (only creating shell)
  • When io/fs.init$after executes later, FileMode is already non-null

Error 2: IR Generation Reuses Condition Variable

Problem: Method type initialization is bound to the same null check condition as parent type creation

Consequence:

  • When parent type already exists (condition is false), method type initialization is skipped
  • Results in func() FileMode never being created
  • DirEntry.Type() method's Imethod.Typ_ is nil

Problem Trigger Chain

1. go/build.init$after executes (Error 1)
   └─> Uses io/fs.FileMode
       └─> Triggers premature FileMode initialization (only creates shell)

2. go/build.init$hasPatch executes
   └─> Initializes dependency packages
       └─> io/fs.init() → io/fs.init$after
           └─> Initializes io/fs.DirEntry interface
               └─> Processes Type() method
                   └─> Needs to create func() FileMode

3. Generated IR in io/fs.init$after (Error 2)
   ├─> Checks if FileMode is null → %443 = false (already exists)
   ├─> Skips FileMode creation block (correct)
   └─> Error: Also skips method type creation block (because %443 is reused)

4. Result
   └─> func() FileMode is never created
       └─> DirEntry.Type()'s Imethod.Typ_ = nil
           └─> Runtime crash

Solutions

Solution 1: Fix Init Order

Change: Adjust compiler logic to ensure init$hasPatch executes before init$after

Effect: Prevents init$after from prematurely using incompletely initialized types

Solution 2: Fix IR Generation Logic

Change: Allow method type initialization to have independent null check, not dependent on parent type condition

Reason:

  1. Method types have their own null check mechanism
  2. Method type initialization should not depend on whether parent type already exists
  3. Even when parent type already exists, method types still need to be initialized

Summary

This is a bug that requires two compiler errors to exist simultaneously to trigger:

  1. Init order error: Causes FileMode to be prematurely initialized
  2. IR generation logic error: Causes method type initialization to be incorrectly skipped

Fixing either one alone can resolve the surface problem, but both need to be fixed to completely resolve the root issue.


Report Generated: 2025-01
LLGo Version: main branch

@gemini-code-assist
Copy link

Summary of Changes

Hello @luoliwoshang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical bug in overlay packages that caused segmentation faults due to an incorrect initialization order. Specifically, the init$after function was executing before init$hasPatch, leading to attempts to access uninitialized cross-package types. The change modifies the notInit() function to correctly recognize $hasPatch calls as initialization routines, thereby enforcing the proper execution sequence (init$hasPatch then init$after) and stabilizing overlay package operations.

Highlights

  • Initialization Order Fix: Corrects an incorrect initialization order in overlay packages where init$after was being called before init$hasPatch. This led to segmentation faults when init$after attempted to access uninitialized cross-package types.
  • Root Cause Identified: The notInit() function in ssa/stmt_builder.go was not recognizing $hasPatch as an init-like call, causing init$after to be inserted prematurely in the LLVM IR.
  • Enhanced notInit() Function: The notInit() function has been updated to correctly identify both .init and $hasPatch suffixes, ensuring that init$hasPatch calls are processed before init$after calls.
  • Crash Resolution: This fix resolves crashes observed in go/build and other overlay packages, ensuring type dependencies are properly initialized before use.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly addresses a critical segmentation fault caused by an incorrect initialization order for overlay packages. The fix in ssa/stmt_builder.go is direct and effective. I have one suggestion to improve the long-term maintainability of the notInit function by making the suffix check more extensible. Overall, this is a good fix.

Comment on lines 159 to 162
name := fn.Name()
// Check if this is an init call (.init suffix) or hasPatch call ($hasPatch suffix)
// Both should be considered as initialization calls that need to run before init$after
return !strings.HasSuffix(name, ".init") && !strings.HasSuffix(name, "$hasPatch")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This change is correct and fixes the bug. For better long-term maintainability, you could consider making this check more extensible. Instead of adding && !strings.HasSuffix(...) for each new init-like suffix, iterating over a slice of suffixes would make it easier to add more in the future.

			name := fn.Name()
			// Check for init-like suffixes. These calls must run before init$after.
			for _, suffix := range []string{".init", "$hasPatch"} {
				if strings.HasSuffix(name, suffix) {
					return false
				}
			}
			return true

@xgopilot
Copy link

xgopilot bot commented Nov 11, 2025

Code Review Summary

This PR effectively resolves a critical initialization order bug in overlay packages. The fix is minimal, precise, and well-documented with inline comments. All security, performance, and code quality reviews indicate this is a solid implementation.

✅ Approved - The change maintains high code quality standards while fixing the segmentation fault issue in go/build overlay packages.

Key strengths:

  • Minimal scope with clear intent
  • Proper inline documentation explaining the overlay package fix
  • No performance or security concerns
  • Follows existing code patterns consistently

@luoliwoshang luoliwoshang force-pushed the fix-overlay-init-order branch 3 times, most recently from b6eca18 to c485e2c Compare November 12, 2025 07:34
@codecov
Copy link

codecov bot commented Nov 12, 2025

Codecov Report

❌ Patch coverage is 96.29630% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.27%. Comparing base (2363d28) to head (3c898ef).
⚠️ Report is 60 commits behind head on main.

Files with missing lines Patch % Lines
ssa/abitype.go 96.29% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1390      +/-   ##
==========================================
- Coverage   90.98%   90.27%   -0.71%     
==========================================
  Files          43       43              
  Lines       11287    11404     +117     
==========================================
+ Hits        10269    10295      +26     
- Misses        859      949      +90     
- Partials      159      160       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Add a minimal test case that reproduces the conditional variable reuse
bug in type initialization.

The test demonstrates:
- A named type (MyType) that is initialized early
- An interface (MyInterface) with a method returning MyType
- The compiler incorrectly reuses the parent type's null check
  condition to control method type initialization

In the generated IR (out.ll):
- Line 163-164: Defines %9 = (MyType == null)
- Line 182: Reuses the same %9 to control InitNamed call
- When MyType already exists, method initialization is skipped

This is the same bug pattern as io/fs.FileMode in the reported issue,
but in a much simpler form for easier understanding and testing.

Related: goplus#1389

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@luoliwoshang luoliwoshang force-pushed the fix-overlay-init-order branch from c485e2c to 113aa06 Compare November 12, 2025 07:48
luoliwoshang and others added 3 commits November 15, 2025 16:42
Fixes goplus#1389 by introducing lazy loader functions that guarantee type initialization regardless of control flow.

Previously, nested type initializations (e.g., `func() MyType`) were incorrectly placed inside conditional blocks (Phase2 of parent types). This caused segfaults when the parent type was already initialized, as the nested type's initialization would be skipped.

Changes:
- Add `createTypeLoader()` to generate `__llgo_load_<typename>` functions
- Each loader performs null check and initializes the type on first call
- Modify `loadType()` to return call expression instead of global variable
- All type references now become `call @__llgo_load_xxx()` instead of `load @xxx`
- Add test cases for SetBlockEx behavior to document the temporary block switching pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Regenerated all test IR files using gentests after implementing lazy type loading mechanism. Key changes in IR:
- All type references now use `call @__llgo_load_<typename>()` instead of `load @<typename>`
- Each type has a dedicated loader function with null check
- Removed `init$after` function as type initialization is now lazy
- Type initialization happens on-demand in loader functions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixes goplus#1389 by introducing lazy loader functions that guarantee type
initialization regardless of control flow.

Changes:
- Delete redundant abiTypeInitInline function
- Rename typeLoaderName → abiTypeLoaderName
- Rename createTypeLoader → abiTypeInitLoader
- Modify loadType() to call loaders in both init$after and current function
- Each type gets independent __llgo_load_<typename> function
- Loader functions perform null check and initialize on first call
- Update all test outputs to reflect new IR generation

This fixes the bug where nested type initializations were incorrectly
placed in conditional blocks and could be skipped.
@luoliwoshang luoliwoshang changed the title fix(ssa): correct init order for overlay packages feat(ssa): implement lazy loading for type initialization to fix #1389 Nov 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant