Files
visor/tools/zap/zap.go
T
zeekayandHanzo Dev 3da05638df State the ZAP wire of every urpc payload
urpc encodes with encoding/json, which walks the payload on every call: a
realistic control.ExecArgs costs 7084ns and 10 allocations to cross. ZAP
reads a field at a constant offset out of the buffer that arrived, so the
same payload is 740ns and one allocation -- but only if the offsets are
stated, and a stated offset that is wrong is silent corruption rather than
a compile error. So state them from the types themselves.

//tools/zap reflects over the payloads at build time and writes the wire
as Go source with the offsets as constants. It lives inside Bazel because
pkg/sentry/control cannot be imported from outside it: the *_go_proto
packages it depends on have no source in the tree.

Six payloads had no layout to state. A map, an interface and a type from
another module have no offsets, so each is answered in the type:

  SaveOpts.Metadata and StartContainerArgs.InitialCgroups/Limits cross as
  lists of pairs; boot.Route.Destination restates net.IPNet as Subnet;
  OpenMountArgs restates the OCI mount fields the call actually uses;
  StartArgs.Spec and SinkConfig.Config carry their documents as the bytes
  they arrived as; MetricsRegistrationResponse carries the protocol
  buffer the same way; and seccheck's session options become a type
  instead of a map of any.

ExecArgs and MetricsExportData still have none: ExecArgs holds live
kernel objects behind an interface, and MetricsExportData reaches a
prometheus snapshot whose labels are a map.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:10:33 -07:00

225 lines
7.5 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.
// Command zap states the ZAP wire of every urpc payload, reading it from the
// type and writing it as Go source with the offsets as constants. Nothing
// reflects when a call is served.
//
// Run it after changing a payload:
//
// make zap
//
// A payload it cannot state is named on stderr and left alone. Answering that
// is a change to the type: a map, an interface or a fixed array of anything but
// bytes has no offset, so there is no layout to write down.
//
// The layouts live in the packages that declare the payloads, so a type change
// can leave a layout that no longer compiles — which is the point, since a
// stale offset is silent corruption. `make zap` empties each layout before
// regenerating it, so the package the generator reflects over is the package
// the types are in today.
package main
import (
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/zap-proto/zip"
"gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/state/stateipc"
"gvisor.dev/gvisor/pkg/urpc"
"gvisor.dev/gvisor/runsc/boot"
"gvisor.dev/gvisor/runsc/container"
)
// module is the import path this repository serves. A layout outside it names a
// package we cannot write to, which is a refusal and not an output.
const module = "gvisor.dev/gvisor/"
// roots are the arguments and results of every method urpc serves. Everything
// they reach is stated with them, so only the top of each call appears here.
//
// A payload missing from this list states no wire, and the call that carries it
// fails naming the type. Adding a method means adding its two payloads.
var roots = []reflect.Type{
// pkg/urpc: the envelope itself.
reflect.TypeFor[urpc.FilePayload](),
// pkg/sentry/control.
reflect.TypeFor[control.CatOpts](),
reflect.TypeFor[control.CgroupsReadArgs](),
reflect.TypeFor[control.CgroupsResults](),
reflect.TypeFor[control.CgroupsWriteArgs](),
reflect.TypeFor[control.ContainerArgs](),
reflect.TypeFor[control.EventsOpts](),
reflect.TypeFor[control.ExecArgs](),
reflect.TypeFor[control.GetRegisteredMetricsOpts](),
reflect.TypeFor[control.LoggingArgs](),
reflect.TypeFor[control.MemoryUsage](),
reflect.TypeFor[control.MemoryUsageFile](),
reflect.TypeFor[control.MemoryUsageFileOpts](),
reflect.TypeFor[control.MemoryUsageOpts](),
reflect.TypeFor[control.MetricsExportData](),
reflect.TypeFor[control.MetricsExportOpts](),
reflect.TypeFor[control.MetricsRegistrationResponse](),
reflect.TypeFor[control.MountOpts](),
reflect.TypeFor[control.Process](),
reflect.TypeFor[control.PsArgs](),
reflect.TypeFor[control.ReadOpts](),
reflect.TypeFor[control.SaveOpts](),
reflect.TypeFor[control.SaveRestoreExecOpts](),
reflect.TypeFor[control.SignalContainerArgs](),
reflect.TypeFor[control.SignalProcessArgs](),
reflect.TypeFor[control.StartContainerArgs](),
reflect.TypeFor[control.TarRootfsUpperLayerOpts](),
reflect.TypeFor[control.UmountOpts](),
reflect.TypeFor[control.UsageReduceOpts](),
reflect.TypeFor[control.UsageReduceOutput](),
// pkg/sentry/control, the profile options.
reflect.TypeFor[control.BlockProfileOpts](),
reflect.TypeFor[control.CPUProfileOpts](),
reflect.TypeFor[control.GoroutineProfileOpts](),
reflect.TypeFor[control.HeapProfileOpts](),
reflect.TypeFor[control.MutexProfileOpts](),
reflect.TypeFor[control.TraceProfileOpts](),
// pkg/sentry/state/stateipc.
reflect.TypeFor[stateipc.CloseRequest](),
reflect.TypeFor[stateipc.CloseResponse](),
reflect.TypeFor[stateipc.OpenRequest](),
reflect.TypeFor[stateipc.OpenResponse](),
reflect.TypeFor[stateipc.RegisterClientFileRequest](),
reflect.TypeFor[stateipc.RegisterClientFileResponse](),
// runsc/boot.
reflect.TypeFor[boot.CreateArgs](),
reflect.TypeFor[boot.CreateLinksAndRoutesArgs](),
reflect.TypeFor[boot.CreateTraceSessionArgs](),
reflect.TypeFor[boot.FSSaveArgs](),
reflect.TypeFor[boot.InitPluginStackArgs](),
reflect.TypeFor[boot.MountArgs](),
reflect.TypeFor[boot.PortForwardOpts](),
reflect.TypeFor[boot.Savings](),
reflect.TypeFor[boot.SignalArgs](),
reflect.TypeFor[boot.StartArgs](),
reflect.TypeFor[boot.WaitFSRestoreArgs](),
reflect.TypeFor[boot.WaitPIDArgs](),
// runsc/container.
reflect.TypeFor[container.OpenMountArgs](),
reflect.TypeFor[container.OpenMountResult](),
}
func main() {
dir := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
if dir == "" {
fmt.Fprintln(os.Stderr, "zap: run me with bazel run //tools/zap")
os.Exit(1)
}
// One root at a time, so a refusal names the payload that carries it
// instead of stopping the run at the first one.
var take []reflect.Type
var refused []string
for _, r := range roots {
if _, err := zip.Layouts(r); err != nil {
refused = append(refused, fmt.Sprintf("%s.%s: %s", short(r.PkgPath()), r.Name(), trim(err)))
continue
}
take = append(take, r)
}
layouts, err := zip.Layouts(take...)
if err != nil {
fmt.Fprintln(os.Stderr, "zap:", err)
os.Exit(1)
}
var wrote, outside []string
for _, l := range layouts {
rel, ok := strings.CutPrefix(l.Path, module)
if !ok {
outside = append(outside, fmt.Sprintf("%s: another module owns it (%s)", l.Path, strings.Join(l.Types, ", ")))
continue
}
if _, err := os.Stat(filepath.Join(dir, rel)); err != nil {
outside = append(outside, fmt.Sprintf("%s: no source here, bazel writes it (%s)", rel, strings.Join(l.Types, ", ")))
continue
}
path := filepath.Join(dir, rel, "zap.go")
if err := os.WriteFile(path, append([]byte(license), l.Source...), 0644); err != nil {
fmt.Fprintln(os.Stderr, "zap:", err)
os.Exit(1)
}
wrote = append(wrote, fmt.Sprintf("%s (%d types)", rel, len(l.Types)))
}
sort.Strings(wrote)
for _, w := range wrote {
fmt.Println("wrote", w)
}
if len(outside) > 0 {
sort.Strings(outside)
fmt.Fprintln(os.Stderr, "\nreached, but not ours to write:")
for _, o := range outside {
fmt.Fprintln(os.Stderr, " ", o)
}
}
if len(refused) > 0 {
sort.Strings(refused)
fmt.Fprintln(os.Stderr, "\nno layout:")
for _, r := range refused {
fmt.Fprintln(os.Stderr, " ", r)
}
os.Exit(1)
}
}
// short drops the module prefix from an import path so a verdict reads as the
// package a reader would open.
func short(path string) string {
if rel, ok := strings.CutPrefix(path, module); ok {
return rel
}
return path
}
// trim drops the emitter's own prefix from a refusal: the reader already knows
// which generator is speaking.
func trim(err error) string {
return strings.TrimPrefix(err.Error(), "zip: ")
}
const license = `// 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.
`