Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 70 additions & 9 deletions src/cmd/compile/internal/ssa/llvmdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -498,27 +500,31 @@ func llvmDataSymbolKindSupported(kind objabi.SymKind) bool {
}
}

func llvmDataType(s *obj.LSym) llvm.Type {
fields := llvmDataFields(s, nil, nil)
func llvmDataType(s *obj.LSym, contents []byte) llvm.Type {
fields := llvmDataFields(s, contents, nil, nil)
return GlobalCtxt.ConstStruct(fields, true).Type()
}

func llvmDataInitializer(s *obj.LSym, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) llvm.Value {
fields := llvmDataFields(s, globals, data)
func llvmDataInitializer(s *obj.LSym, contents []byte, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) llvm.Value {
fields := llvmDataFields(s, contents, globals, data)
return GlobalCtxt.ConstStruct(fields, true)
}

// llvmDataFields models an LSym as a packed aggregate. Byte runs preserve the
// frontend's exact layout while relocation slots retain their LLVM constants.
// Using a packed aggregate is necessary because descriptors contain 32-bit
// offsets adjacent to pointer-sized fields.
func llvmDataFields(s *obj.LSym, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) []llvm.Value {
dataSize := int(s.Size)
if dataSize < len(s.P) {
dataSize = len(s.P)
func llvmDataFields(s *obj.LSym, contents []byte, globals map[*obj.LSym]llvm.Value, data map[*obj.LSym]bool) []llvm.Value {
dataSize64 := s.Size
if dataSize64 < int64(len(contents)) {
dataSize64 = int64(len(contents))
}
if dataSize64 < 0 || uint64(dataSize64) > uint64(^uint(0)>>1) {
base.Fatalf("invalid LLVM data symbol size %d for %s", dataSize64, s.Name)
}
dataSize := int(dataSize64)
bytes := make([]byte, dataSize)
copy(bytes, s.P)
copy(bytes, contents)
relocs := llvmDataStorageRelocs(s)
sort.Slice(relocs, func(i, j int) bool { return relocs[i].Off < relocs[j].Off })

Expand Down Expand Up @@ -549,6 +555,61 @@ func llvmDataFields(s *obj.LSym, globals map[*obj.LSym]llvm.Value, data map[*obj
return fields
}

func (l *llvmDataLowerer) dataBytes(s *obj.LSym) []byte {
if s.File() == nil {
return s.P
}
if data, ok := l.fileData[s]; ok {
return data
}
data, err := readLLVMFileData(s)
if err != nil {
base.Fatalf("reading file-backed LLVM data symbol %s: %v", s.Name, err)
}
l.fileData[s] = data
return data
}

// readLLVMFileData materializes the same file-backed LSym bytes that the
// native Go object writer streams into its object. LLVM constants require the
// complete initializer in memory, so validate both metadata sizes and the
// actual EOF while reading it once for the lowerer's type and initializer.
func readLLVMFileData(s *obj.LSym) ([]byte, error) {
file := s.File()
if file == nil {
return s.P, nil
}
if s.P != nil {
return nil, fmt.Errorf("file-backed symbol also has %d inline bytes", len(s.P))
}
if file.Size != s.Size {
return nil, fmt.Errorf("file metadata length %d does not match symbol size %d", file.Size, s.Size)
}
if file.Size < 0 || uint64(file.Size) > uint64(^uint(0)>>1) {
return nil, fmt.Errorf("invalid file length %d", file.Size)
}
f, err := os.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()

data := make([]byte, int(file.Size))
if _, err := io.ReadFull(f, data); err != nil {
return nil, fmt.Errorf("copy %s: expected %d bytes: %w", file.Name, file.Size, err)
}
var extra [1]byte
n, err := io.ReadFull(f, extra[:])
switch {
case n == 0 && err == io.EOF:
return data, nil
case err == nil:
return nil, fmt.Errorf("copy %s: file is longer than expected %d bytes", file.Name, file.Size)
default:
return nil, fmt.Errorf("copy %s after %d bytes: %w", file.Name, file.Size, err)
}
}

func llvmDataBytes(b []byte) llvm.Value {
return GlobalCtxt.ConstString(string(b), false)
}
Expand Down
6 changes: 4 additions & 2 deletions src/cmd/compile/internal/ssa/llvmtypeddata.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type llvmDataLowerer struct {
externalRoots map[*obj.LSym]bool
lowered map[*obj.LSym]bool
values map[*obj.LSym]llvm.Value
fileData map[*obj.LSym][]byte
anonymousCount int
runtimeTypes map[*types.Type]llvm.Type
descriptorTypes map[*obj.LSym]llvm.Type
Expand All @@ -38,6 +39,7 @@ func newLLVMDataLowerer(data map[*obj.LSym]bool) *llvmDataLowerer {
externalRoots: make(map[*obj.LSym]bool),
lowered: make(map[*obj.LSym]bool),
values: make(map[*obj.LSym]llvm.Value),
fileData: make(map[*obj.LSym][]byte),
runtimeTypes: make(map[*types.Type]llvm.Type),
descriptorTypes: make(map[*obj.LSym]llvm.Type),
namedRuntimeType: make(map[*types.Type]bool),
Expand All @@ -51,7 +53,7 @@ func (l *llvmDataLowerer) dataType(s *obj.LSym) llvm.Type {
if s.ItabInfo() != nil {
return l.itabType(s)
}
return llvmDataType(s)
return llvmDataType(s, l.dataBytes(s))
}

func (l *llvmDataLowerer) dataInitializer(s *obj.LSym, globals map[*obj.LSym]llvm.Value) llvm.Value {
Expand All @@ -61,7 +63,7 @@ func (l *llvmDataLowerer) dataInitializer(s *obj.LSym, globals map[*obj.LSym]llv
if s.ItabInfo() != nil {
return l.itabInitializer(s, globals)
}
return llvmDataInitializer(s, globals, l.data)
return llvmDataInitializer(s, l.dataBytes(s), globals, l.data)
}

func llvmDescriptorGoType(s *obj.LSym) *types.Type {
Expand Down
70 changes: 70 additions & 0 deletions src/cmd/compile/internal/ssa/ssa2llvm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
package ssa

import (
"bytes"
"os"
"strings"
"testing"

Expand Down Expand Up @@ -182,6 +184,74 @@ func TestLLVMGoObjCompilerUsedOnlyKeepsExternalDataRoots(t *testing.T) {
}
}

func TestLLVMFileBackedDataLowering(t *testing.T) {
payload := append(bytes.Repeat([]byte("file-backed-data-"), 80), []byte("GOALLC_FILE_END")...)
path := t.TempDir() + "/payload"
if err := os.WriteFile(path, payload, 0o600); err != nil {
t.Fatal(err)
}

s := &obj.LSym{Name: "test.file.backed", Type: objabi.SRODATA, Size: int64(len(payload))}
file := s.NewFileInfo()
file.Name = path
file.Size = int64(len(payload))
lowerer := newLLVMDataLowerer(map[*obj.LSym]bool{s: true})

module := GlobalCtxt.NewModule("file_backed_data")
t.Cleanup(module.Dispose)
g := llvm.AddGlobal(module, lowerer.dataType(s), s.Name)
g.SetInitializer(lowerer.dataInitializer(s, map[*obj.LSym]llvm.Value{s: g}))
if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil {
t.Fatalf("LLVM verifier rejected file-backed data: %v\n%s", err, module.String())
}

ir := module.String()
if strings.Contains(ir, "zeroinitializer") {
t.Fatalf("file-backed LLVM data was lowered as zeros:\n%s", ir)
}
if !strings.Contains(ir, "GOALLC_FILE_END") {
t.Fatalf("file-backed LLVM data lost its payload:\n%s", ir)
}
}

func TestReadLLVMFileDataRejectsLengthMismatches(t *testing.T) {
path := t.TempDir() + "/payload"
if err := os.WriteFile(path, []byte("12345"), 0o600); err != nil {
t.Fatal(err)
}

newSymbol := func(symbolSize, fileSize int64) *obj.LSym {
s := &obj.LSym{Name: "test.file.backed", Type: objabi.SRODATA, Size: symbolSize}
file := s.NewFileInfo()
file.Name = path
file.Size = fileSize
return s
}
for _, tc := range []struct {
name string
symbolSize int64
fileSize int64
want string
}{
{name: "metadata", symbolSize: 5, fileSize: 4, want: "metadata length 4 does not match symbol size 5"},
{name: "short", symbolSize: 6, fileSize: 6, want: "expected 6 bytes"},
{name: "long", symbolSize: 4, fileSize: 4, want: "longer than expected 4 bytes"},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := readLLVMFileData(newSymbol(tc.symbolSize, tc.fileSize))
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("readLLVMFileData error = %v, want substring %q", err, tc.want)
}
})
}

s := newSymbol(5, 5)
s.P = []byte("12345")
if _, err := readLLVMFileData(s); err == nil || !strings.Contains(err.Error(), "also has 5 inline bytes") {
t.Fatalf("readLLVMFileData inline/file conflict error = %v", err)
}
}

func TestLLVMUntypedABI0FunctionAddressCreatesFunctionDeclaration(t *testing.T) {
oldModule := CurrentModule
oldLowerer := currentLLVMDataLowerer
Expand Down
61 changes: 61 additions & 0 deletions test/llvm_file_backed_embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// run

// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import _ "embed"

//go:embed llvm_file_backed_embed.go
var llvmFileBackedSource string

//go:embed llvm_file_backed_embed.go
var llvmFileBackedBytes []byte

const llvmFileBackedSentinel = "GOALLC_FILE_BACKED_EMBED_SENTINEL"

func containsFileBackedSentinel(data, sentinel string) bool {
for i := 0; i+len(sentinel) <= len(data); i++ {
if data[i:i+len(sentinel)] == sentinel {
return true
}
}
return false
}

func main() {
if len(llvmFileBackedSource) <= 1024 {
panic("fixture did not use a file-backed linker symbol")
}
if !containsFileBackedSentinel(llvmFileBackedSource, llvmFileBackedSentinel) {
panic("file-backed linker symbol lost its contents")
}
if len(llvmFileBackedBytes) != len(llvmFileBackedSource) || !containsFileBackedSentinel(string(llvmFileBackedBytes), llvmFileBackedSentinel) {
panic("writable file-backed linker symbol lost its contents")
}
original := llvmFileBackedSource[0]
llvmFileBackedBytes[0] ^= 0xff
if llvmFileBackedBytes[0] == original || llvmFileBackedSource[0] != original {
panic("writable file-backed linker symbol did not retain separate storage")
}
}

// Keep this checked-in fixture larger than staticdata.fileStringSym's 1 KiB
// in-memory threshold. The embedded file is this source itself, so testdir's
// ordinary Go command path supplies a real embed configuration while the test
// remains a single-file run recipe. The padding is deliberately readable and
// stable: it is part of the compiler input and lets the runtime assertion
// distinguish a correctly materialized LLVM constant from a zero initializer.
//
// LLVM file-backed data padding 01: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 02: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 03: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 04: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 05: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 06: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 07: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 08: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 09: abcdefghijklmnopqrstuvwxyz0123456789
// LLVM file-backed data padding 10: abcdefghijklmnopqrstuvwxyz0123456789
1 change: 1 addition & 0 deletions test/llvm_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"llvm_range_statepoint_gc.go": "wide array range derives each element from a relocatable base across repeated statepoints and stack growth",
"llvm_memeq.go": "raw memory equality for true, false, different-length, and offset substring inputs",
"llvm_move.go": "overlap-safe, aligned, and runtime-helper memory moves",
"llvm_file_backed_embed.go": "large string and byte-slice go:embed data preserve file-backed LSym bytes and storage semantics through LLVM GoObj linking and execution",
"llvm_private_string.go": "frontend private string constant materialization, GoObj linking, and execution",
"llvm_zero_sized_symbols.go": "zero-value strings and zero-sized interface data through GoObj linking and execution",
"llvm_slicemask.go": "slice start, middle, and zero-capacity end boundary semantics",
Expand Down
Loading