Files
visor/pkg/sentry/pgalloc/pgalloc_64k_test.go
Tao Lin a9f716e19e Address review feedback
- Update copyright year to 2026 for all new files
- Update comment in hostarch_arm64.go to mention 64K page support
  via pagesize_64k build tag
- Add PageSize checks in KVM and Systrap New() constructors to
  fail early on non-4K page sizes
2026-03-04 14:00:13 -08:00

469 lines
12 KiB
Go

// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build arm64 && pagesize_64k
package pgalloc
import (
"testing"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/sentry/memmap"
)
const (
page = hostarch.PageSize
hugepage = hostarch.HugePageSize
)
// existingSegment represents a range of pages in a test MemoryFile that is not
// void or free.
type existingSegment struct {
start uint64
end uint64
state int
}
// Possible values for existingSegment.state:
const (
existingUnspecified = iota
existingUsed
existingWaste
existingReleasing // or sub-releasing
)
// TestFindAllocatable64K tests allocation for 64K page size.
// Note: For 64K pages, hugepage is 512MB and chunkSize is 1GB,
// so chunkSize only holds 2 hugepages. Tests are adjusted accordingly.
func TestFindAllocatable64K(t *testing.T) {
for _, test := range []struct {
name string
// Initial state:
chunkHuge []bool
existing []existingSegment
// Allocation parameters:
length uint64
huge bool
recycle bool
dir Direction
// Expected outcome:
want uint64
}{
{
name: "initial small allocation, bottom-up",
length: page,
want: 0,
},
{
name: "initial small allocation, top-down",
length: page,
dir: TopDown,
want: chunkSize - page,
},
{
name: "initial small allocation, multiple pages, top-down",
length: 2 * page,
dir: TopDown,
want: chunkSize - 2*page,
},
{
name: "initial small allocation, recycling enabled, bottom-up",
length: page,
recycle: true,
want: 0,
},
{
name: "initial huge allocation, bottom-up",
length: hugepage,
huge: true,
want: 0,
},
{
name: "initial huge allocation, top-down",
length: hugepage,
huge: true,
dir: TopDown,
want: chunkSize - hugepage,
},
{
name: "initial huge allocation, recycling enabled, bottom-up",
length: hugepage,
huge: true,
recycle: true,
want: 0,
},
{
name: "huge allocation uses huge pages in new chunk",
chunkHuge: []bool{false},
length: hugepage,
huge: true,
want: chunkSize,
},
{
name: "huge allocation uses huge pages in existing chunk",
chunkHuge: []bool{false, true},
length: hugepage,
huge: true,
want: chunkSize,
},
{
name: "hugepage-sized non-huge allocation uses small pages in new chunk",
chunkHuge: []bool{true},
length: hugepage,
want: chunkSize,
},
{
name: "hugepage-sized non-huge allocation uses small pages in existing chunk",
chunkHuge: []bool{true, false},
length: hugepage,
want: chunkSize,
},
{
name: "bottom-up small allocation begins at start of file",
chunkHuge: []bool{false},
existing: []existingSegment{
{page, 2 * page, existingUsed},
},
length: page,
want: 0,
},
{
name: "top-down small allocation begins at end of last chunk",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - 2*page, chunkSize - page, existingUsed},
},
length: page,
dir: TopDown,
want: chunkSize - page,
},
{
name: "bottom-up huge allocation begins at start of file",
chunkHuge: []bool{true},
existing: []existingSegment{
{hugepage, chunkSize, existingUsed},
},
length: hugepage,
huge: true,
want: 0,
},
{
name: "top-down huge allocation begins at end of last chunk",
chunkHuge: []bool{true},
existing: []existingSegment{
{0, hugepage, existingUsed},
},
length: hugepage,
huge: true,
dir: TopDown,
want: chunkSize - hugepage,
},
{
name: "bottom-up small allocation can extend multiple chunks",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize/2 - page, chunkSize / 2, existingUsed},
},
length: 2*chunkSize + page,
want: chunkSize / 2,
},
{
name: "top-down small allocation can extend multiple chunks",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize/2 - page, chunkSize / 2, existingUsed},
},
length: 2*chunkSize + page,
dir: TopDown,
want: chunkSize - page,
},
{
name: "bottom-up small allocation finds first free gap",
chunkHuge: []bool{false},
existing: []existingSegment{
{0, page, existingUsed},
{2 * page, 3 * page, existingUsed},
},
length: page,
want: page,
},
{
name: "top-down small allocation finds last free gap",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - page, chunkSize, existingUsed},
{chunkSize - 3*page, chunkSize - 2*page, existingUsed},
},
length: page,
dir: TopDown,
want: chunkSize - 2*page,
},
{
name: "bottom-up huge allocation finds first free gap",
chunkHuge: []bool{true, true}, // Need 2 chunks for this test
existing: []existingSegment{
{0, hugepage, existingUsed},
},
length: hugepage,
huge: true,
want: hugepage,
},
{
name: "bottom-up small allocation skips undersized free gap",
chunkHuge: []bool{false},
existing: []existingSegment{
{0, page, existingUsed},
{2 * page, 3 * page, existingUsed},
},
length: 2 * page,
want: 3 * page,
},
{
name: "top-down small allocation skips undersized free gap",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - page, chunkSize, existingUsed},
{chunkSize - 3*page, chunkSize - 2*page, existingUsed},
},
length: 2 * page,
dir: TopDown,
want: chunkSize - 5*page,
},
{
name: "recycling bottom-up small allocation skips used pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{0, page, existingUsed},
},
length: page,
recycle: true,
want: page,
},
{
name: "recycling top-down small allocation skips used pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - page, chunkSize, existingUsed},
},
length: page,
recycle: true,
dir: TopDown,
want: chunkSize - 2*page,
},
{
name: "recycling bottom-up huge allocation skips used pages",
chunkHuge: []bool{true, true}, // Need 2 chunks
existing: []existingSegment{
{0, hugepage, existingUsed},
},
length: hugepage,
huge: true,
recycle: true,
want: hugepage,
},
{
name: "non-recycling bottom-up small allocation skips waste pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{0, page, existingWaste},
},
length: page,
want: page,
},
{
name: "non-recycling top-down small allocation skips waste pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - page, chunkSize, existingWaste},
},
length: page,
dir: TopDown,
want: chunkSize - 2*page,
},
{
name: "non-recycling bottom-up huge allocation skips waste pages",
chunkHuge: []bool{true, true}, // Need 2 chunks
existing: []existingSegment{
{0, hugepage, existingWaste},
},
length: hugepage,
huge: true,
want: hugepage,
},
{
name: "recycling bottom-up small allocation recycles waste pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{0, page, existingWaste},
},
length: page,
recycle: true,
want: 0,
},
{
name: "recycling top-down small allocation recycles waste pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - page, chunkSize, existingWaste},
},
length: page,
recycle: true,
dir: TopDown,
want: chunkSize - page,
},
{
name: "recycling bottom-up huge allocation recycles waste pages",
chunkHuge: []bool{true},
existing: []existingSegment{
{0, hugepage, existingWaste},
},
length: hugepage,
huge: true,
recycle: true,
want: 0,
},
{
name: "non-recycling bottom-up small allocation skips releasing pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{0, page, existingReleasing},
},
length: page,
want: page,
},
{
name: "non-recycling top-down small allocation skips releasing pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - page, chunkSize, existingReleasing},
},
length: page,
dir: TopDown,
want: chunkSize - 2*page,
},
{
name: "non-recycling bottom-up huge allocation skips releasing pages",
chunkHuge: []bool{true, true}, // Need 2 chunks
existing: []existingSegment{
{0, hugepage, existingReleasing},
},
length: hugepage,
huge: true,
want: hugepage,
},
{
name: "recycling bottom-up small allocation skips releasing pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{0, page, existingReleasing},
},
length: page,
recycle: true,
want: page,
},
{
name: "recycling top-down small allocation skips releasing pages",
chunkHuge: []bool{false},
existing: []existingSegment{
{chunkSize - page, chunkSize, existingReleasing},
},
length: page,
recycle: true,
dir: TopDown,
want: chunkSize - 2*page,
},
{
name: "recycling bottom-up huge allocation skips releasing pages",
chunkHuge: []bool{true, true}, // Need 2 chunks
existing: []existingSegment{
{0, hugepage, existingReleasing},
},
length: hugepage,
huge: true,
recycle: true,
want: hugepage,
},
} {
t.Run(test.name, func(t *testing.T) {
// Build the fake MemoryFile.
f := &MemoryFile{
opts: MemoryFileOpts{
ExpectHugepages: true,
DisableMemoryAccounting: true,
},
}
f.initFields()
chunks := make([]chunkInfo, len(test.chunkHuge))
for i, huge := range test.chunkHuge {
chunks[i].huge = huge
chunkFR := memmap.FileRange{uint64(i) * chunkSize, uint64(i+1) * chunkSize}
if huge {
f.unfreeHuge.RemoveRange(chunkFR)
} else {
f.unfreeSmall.RemoveRange(chunkFR)
}
}
f.chunks.Store(&chunks)
for _, es := range test.existing {
f.forEachChunk(memmap.FileRange{es.start, es.end}, func(chunk *chunkInfo, chunkFR memmap.FileRange) bool {
unwaste, unfree := &f.unwasteSmall, &f.unfreeSmall
if chunk.huge {
unwaste, unfree = &f.unwasteHuge, &f.unfreeHuge
}
switch es.state {
case existingUsed:
unfree.InsertRange(chunkFR, unfreeInfo{refs: 1})
case existingWaste:
unfree.InsertRange(chunkFR, unfreeInfo{refs: 0})
unwaste.RemoveRange(chunkFR)
case existingReleasing:
unfree.InsertRange(chunkFR, unfreeInfo{refs: 0})
default:
t.Fatalf("existingSegment %+v has unknown state", es)
}
f.memAcct.InsertRange(chunkFR, memAcctInfo{
wasteOrReleasing: es.state != existingUsed,
})
return true
})
}
// Perform the test allocation.
alloc := allocState{
length: test.length,
opts: AllocOpts{
Huge: test.huge,
Dir: test.dir,
},
huge: test.huge,
}
if test.recycle {
alloc.opts.Mode = AllocateCallerIndirectCommit
alloc.willCommit = true
}
fr, err := f.findAllocatableAndMarkUsed(&alloc)
if err != nil {
t.Fatalf("findAllocatableAndMarkUsed(%+v): failed: %v, want: %#x\n%v", alloc, err, test.want, f)
}
if fr.Start != test.want {
t.Errorf("findAllocatableAndMarkUsed(%+v): got: start=%#x, want: %#x\n%v", alloc, fr.Start, test.want, f)
}
if wantEnd := test.want + test.length; fr.End != wantEnd {
t.Errorf("findAllocatableAndMarkUsed(%+v): got: end=%#x, want: %#x\n%v", alloc, fr.End, wantEnd, f)
}
})
}
}