Skip to content

cabi: remove unsafe load-to-memcpy optimization#1607

Merged
xushiwei merged 4 commits intogoplus:mainfrom
luoliwoshang:return/slot
Feb 5, 2026
Merged

cabi: remove unsafe load-to-memcpy optimization#1607
xushiwei merged 4 commits intogoplus:mainfrom
luoliwoshang:return/slot

Conversation

@luoliwoshang
Copy link
Member

@luoliwoshang luoliwoshang commented Feb 3, 2026

Fixed #1604 #1606 #1608

Reproduction Code

func a() []int {
	var t = T{data: []int{1, 2}}
	a := t.data
	t.data = []int{1, 2, 3}
	return a
}

Problem Analysis

IR Before CABI Transformation

define %Slice @command-line-arguments.a() {
_llgo_0:
  %0 = alloca %T, align 8
  ...
  %8 = getelementptr inbounds %T, ptr %0, i32 0, i32 0  ; get address of T.data
  %9 = load %Slice, ptr %8, align 8                     ; load slice into %9 (should return this)
  ...
  %17 = getelementptr inbounds %T, ptr %0, i32 0, i32 0 ; get address of T.data (same as %8)
  store %Slice %16, ptr %17, align 8                    ; modify T.data = []int{1,2,3}
  ret %Slice %9                                         ; return previously loaded %9
}

IR After CABI Transformation (BUG)

define void @command-line-arguments.a(ptr sret(%"github.com/goplus/llgo/runtime/internal/runtime.Slice") %0) {
_llgo_0:
  %1 = alloca %command-line-arguments.T, align 8
  call void @llvm.memset(ptr %1, i8 0, i64 24, i1 false)
  %2 = getelementptr inbounds %command-line-arguments.T, ptr %1, i32 0, i32 0
  %3 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocZ"(i64 16)
  %4 = getelementptr inbounds i64, ptr %3, i64 0
  store i64 1, ptr %4, align 4
  %5 = getelementptr inbounds i64, ptr %3, i64 1
  store i64 2, ptr %5, align 4
  %6 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %3, 0
  %7 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %6, i64 2, 1
  %8 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %7, i64 2, 2
  store %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %8, ptr %2, align 8
  %9 = getelementptr inbounds %command-line-arguments.T, ptr %1, i32 0, i32 0
  %10 = load %"github.com/goplus/llgo/runtime/internal/runtime.Slice", ptr %9, align 8
  %11 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocZ"(i64 24)
  %12 = getelementptr inbounds i64, ptr %11, i64 0
  store i64 1, ptr %12, align 4
  %13 = getelementptr inbounds i64, ptr %11, i64 1
  store i64 2, ptr %13, align 4
  %14 = getelementptr inbounds i64, ptr %11, i64 2
  store i64 3, ptr %14, align 4
  %15 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %11, 0
  %16 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %15, i64 3, 1
  %17 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %16, i64 3, 2
  %18 = getelementptr inbounds %command-line-arguments.T, ptr %1, i32 0, i32 0
  store %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %17, ptr %18, align 8
  call void @llvm.memcpy(ptr %0, ptr %9, i64 24, i1 false)  ; <-- BUG: copies from %9, but %9 content was modified!
  ret void
}

Root Cause

During CABI transformation, when the optimizer sees ret %Slice %9 where %9 is a load instruction, it previously used memcpy(sret_ptr, %9, size) to copy from the load source address.

The bug: %9 and %18 point to the same memory address (T.data field). After the load but before ret, this address content was modified by store %17, ptr %18!

So memcpy copies the modified value {1,2,3} instead of the originally loaded value {1,2}.

Fix

Remove the memcpy optimization, always use store instruction:

; Before fix (BUG):
call void @llvm.memcpy(ptr %sret, ptr %9, i64 24)  ; copies from %9, but %9 content was modified

; After fix (CORRECT):
store %Slice %10, ptr %0, align 8                  ; directly store SSA value %10
ret void

%10 is an SSA value - once defined, it never changes. So using store %10 is safe.

@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 potential compiler optimization issue by adding a new test case. The test is designed to ensure that the compiler correctly handles scenarios involving Named Return Value Optimization (NRVO) and return-slot reuse, specifically when a slice is returned from a function and its underlying source data is modified afterwards. This prevents situations where the returned slice might unexpectedly reflect post-return modifications, thereby safeguarding data integrity and predictable program behavior.

Highlights

  • New Test Case for NRVO/Return-Slot Reuse: Introduced a new test case (cl/_testgo/return-1605) to specifically target and prevent unexpected behavior related to Named Return Value Optimization (NRVO) and the reuse of return slots.
  • Slice Data Integrity Verification: The test verifies that when a slice is returned from a function, and the original source data (a struct field) is subsequently modified within the function, the returned slice correctly retains its state at the point of return, avoiding unintended data changes.
  • LLVM IR Confirmation: The generated LLVM IR (out.ll) for the test case confirms that the compiler correctly handles the return value by loading the slice before the source struct's field is updated, ensuring the integrity of the returned data.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • cl/_testgo/return-1605/expect.txt
    • Added a new file containing the expected output "2\n2" for the return-1605 test, confirming the correct behavior of slice length after specific operations.
  • cl/_testgo/return-1605/main.go
    • Added a new Go source file that defines functions a() and b() to demonstrate and test the handling of slices returned from functions where the source struct's data field is modified after the slice is captured for return.
  • cl/_testgo/return-1605/out.ll
    • Added the LLVM Intermediate Representation output for the main.go test file, which serves as a reference to verify that the generated machine code correctly avoids unexpected NRVO or return-slot reuse.
Activity
  • The pull request is marked as "[wip]" (Work In Progress), indicating it is not yet ready for final review or merge.
  • The pull request description is currently empty, suggesting that further details or context might be added by the author.
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 introduces a test case to address potential issues with Named Return Value Optimization (NRVO) and return-slot reuse in the compiler, particularly concerning slice return values. The test correctly verifies that a returned slice remains unchanged even if the source struct's slice field is modified after the return value is captured. My feedback includes suggestions to align the new Go test code with idiomatic language conventions for better readability and maintainability.

}

func a() []int {
var t = T{data: []int{1, 2}}

Choose a reason for hiding this comment

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

medium

In Go, it is more idiomatic to use the short variable declaration := when declaring and initializing a new variable inside a function. This improves code conciseness and readability.

Suggested change
var t = T{data: []int{1, 2}}
t := T{data: []int{1, 2}}

}

func b() ([]int, bool) {
var t = T{data: []int{1, 2}}

Choose a reason for hiding this comment

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

medium

In Go, it is more idiomatic to use the short variable declaration := when declaring and initializing a new variable inside a function. This improves code conciseness and readability.

Suggested change
var t = T{data: []int{1, 2}}
t := T{data: []int{1, 2}}

@xgopilot
Copy link
Contributor

xgopilot bot commented Feb 3, 2026

Code Review Summary

Overall, this is a solid regression test that correctly captures the NRVO/return-slot reuse bug scenario. The test structure follows project conventions with main.go, expect.txt, and out.ll files.

Suggestions:

  1. PR Title Typo: Consider fixing unexpectunexpected and the double space
  2. Variable Naming: In function a(), the local variable a shadows the function name - consider renaming to slice or result for clarity
  3. Issue References: The commit message references Bug: TypeSwitch initialization statement missing when running under llgo when use gogen #1604 and Bug: If statement initialization missing when running under llgo #1606, but the directory is named return-1605. Verify the correct issue numbers are being referenced

No security or performance concerns identified.


func a() []int {
var t = T{data: []int{1, 2}}
a := t.data
Copy link
Contributor

Choose a reason for hiding this comment

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

The local variable a shadows the function name a. While Go allows this, consider renaming to something more descriptive like slice, result, or originalData for improved readability.

Fixes goplus#1608

When transforming functions with sret (structure return) in CABI mode,
the optimizer previously used memcpy from the load source address to
the return address. However, if the source address content is modified
between the load and ret instructions, the memcpy would copy the
modified (wrong) value instead of the original loaded value.

Example of the bug:
```go
func problem() []int {
    var t = T{data: []int{1, 2}}
    a := t.data                   // load from &t.data
    t.data = []int{1, 2, 3}       // modify &t.data
    return a                       // should return [1,2], not [1,2,3]
}
```

The memcpy optimization assumed the source address content remains
unchanged between load and ret, which is not always true.

Solution: Disable the memcpy optimization for AttrPointer returns and
always use store instruction to copy the loaded value directly. This
ensures the correct value (from the load instruction) is returned.

This also fixes the root cause of issue goplus#1604 (TypeSwitch initialization
lost) which was caused by gogen's clearBlockStmt() being affected by
this bug.
Move the regression test for issue goplus#1608/goplus#1604 from cl/_testgo to
_demo/go/return-1605 with proper validation logic that panics if
the returned slice has wrong length or elements.
@codecov
Copy link

codecov bot commented Feb 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.05%. Comparing base (505a3fb) to head (fcdd096).
⚠️ Report is 49 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1607      +/-   ##
==========================================
+ Coverage   91.00%   91.05%   +0.05%     
==========================================
  Files          45       45              
  Lines       11927    11933       +6     
==========================================
+ Hits        10854    10866      +12     
+ Misses        898      892       -6     
  Partials      175      175              

☔ 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 TestIssue1608_AttrPointerReturnNoMemcpy to verify CABI transformation
uses store instead of memcpy for sret returns.

The test creates IR where:
1. A slice is loaded from a struct field
2. The struct field is modified
3. The originally loaded slice is returned

This ensures the fix prevents the memcpy optimization bug where the
source address content could change between load and ret.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@luoliwoshang luoliwoshang changed the title [wip]cl:avoid unexpect NRVO/return-slot reuse cabi: remove unsafe load-to-memcpy optimization Feb 5, 2026
@xushiwei xushiwei merged commit ba56141 into goplus:main Feb 5, 2026
37 checks passed
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.

Bug: TypeSwitch initialization statement missing when running under llgo when use gogen

3 participants