diff --git a/pkg/abi/nvgpu/BUILD b/pkg/abi/nvgpu/BUILD index 5b5c0e275..27e0c382b 100644 --- a/pkg/abi/nvgpu/BUILD +++ b/pkg/abi/nvgpu/BUILD @@ -16,6 +16,9 @@ go_library( "uvm.go", ], marshal = True, - visibility = ["//pkg/sentry:internal"], + visibility = [ + "//pkg/sentry:internal", + "//tools:__subpackages__", + ], deps = ["//pkg/marshal"], ) diff --git a/pkg/abi/nvgpu/frontend.go b/pkg/abi/nvgpu/frontend.go index 2a5e838ee..df8bb9f48 100644 --- a/pkg/abi/nvgpu/frontend.go +++ b/pkg/abi/nvgpu/frontend.go @@ -170,6 +170,7 @@ type NVOS00Parameters struct { // RmAllocParamType should be implemented by all possible parameter types for // NV_ESC_RM_ALLOC. type RmAllocParamType interface { + GetHClass() ClassID GetPAllocParms() P64 GetPRightsRequested() P64 SetPAllocParms(p P64) @@ -203,6 +204,11 @@ type NVOS21Parameters struct { Status uint32 } +// GetHClass implements RmAllocParamType.GetHClass. +func (n *NVOS21Parameters) GetHClass() ClassID { + return n.HClass +} + // GetPAllocParms implements RmAllocParamType.GetPAllocParms. func (n *NVOS21Parameters) GetPAllocParms() P64 { return n.PAllocParms @@ -403,6 +409,11 @@ type NVOS64Parameters struct { _ uint32 } +// GetHClass implements RmAllocParamType.GetHClass. +func (n *NVOS64Parameters) GetHClass() ClassID { + return n.HClass +} + // GetPAllocParms implements RmAllocParamType.GetPAllocParms. func (n *NVOS64Parameters) GetPAllocParms() P64 { return n.PAllocParms diff --git a/pkg/sentry/devices/nvproxy/BUILD b/pkg/sentry/devices/nvproxy/BUILD index 3a4c6836c..1ad80a16e 100644 --- a/pkg/sentry/devices/nvproxy/BUILD +++ b/pkg/sentry/devices/nvproxy/BUILD @@ -54,7 +54,7 @@ go_library( ], visibility = [ "//pkg/sentry:internal", - "//tools/gpu:__subpackages__", + "//tools:__subpackages__", ], deps = [ "//pkg/abi/linux", diff --git a/tools/ioctl_sniffer/BUILD b/tools/ioctl_sniffer/BUILD new file mode 100644 index 000000000..3d4baef98 --- /dev/null +++ b/tools/ioctl_sniffer/BUILD @@ -0,0 +1,39 @@ +load("//tools:defs.bzl", "cc_binary", "go_binary", "proto_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +proto_library( + name = "ioctl", + srcs = ["ioctl.proto"], + visibility = ["//tools/ioctl_sniffer:__subpackages__"], +) + +cc_binary( + name = "ioctl_hook", + srcs = [ + "ioctl_hook.cc", + "ioctl_hook.h", + "sniffer_bridge.cc", + "sniffer_bridge.h", + ], + linkshared = True, + deps = [ + ":ioctl_cc_proto", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_protobuf//src/google/protobuf/io", + ], +) + +go_binary( + name = "run_sniffer", + srcs = ["run_sniffer.go"], + static = True, + deps = [ + "//pkg/log", + "//tools/ioctl_sniffer/sniffer", + ], +) diff --git a/tools/ioctl_sniffer/README.md b/tools/ioctl_sniffer/README.md new file mode 100644 index 000000000..f1ab9a2da --- /dev/null +++ b/tools/ioctl_sniffer/README.md @@ -0,0 +1,52 @@ +# Ioctl Sniffer + +This tool provides a way to profile GPU workloads, sniff out all the Nvidia +`ioctl(2)` calls that are involved, and ultimately filter out the calls that are +currently unsupported by nvproxy. + +This is accomplished by providing a `libioctl_hook.so` shared library, which can +be `LD_PRELOAD`ed and intercepts all `ioctl(2)` calls made. Any calls made to +known Nvidia device files are then captured and parsed by the `sniffer` Go +package. The sniffer compares against nvproxy's list of supported `ioctl(2)` +numbers for the current driver version, and checks if the given call is +contained in the list. For `NV_ESC_RM_CONTROL` and `NV_ESC_RM_ALLOC` calls, it +also extracts the control command and allocation class respectively, and checks +if nvproxy supports them. + +## Usage + +To start, we need to build the shared library and Go binary: + +``` +make copy TARGETS=//tools/ioctl_sniffer:run_sniffer DESTINATION=bin/ +make copy TARGETS=//tools/ioctl_sniffer:ioctl_hook DESTINATION=bin/ +``` + +Once we have the binary, we can hook into any GPU workload by passing the +corresponding command to run it to `run_sniffer`, like so: + +``` +./run_sniffer nvidia-smi +``` + +This should run the workload as normal and provide an output of all the +unsupported `ioctl(2)` calls it detected at the end: + +``` +============== Unsupported ioctls ============== +Frontend: +UVM: +Control: + Control ioctl: request=0xc020462a [nr=0x2a (42), cmd=0x20810110 (545325328)] => ret=0 + Control ioctl: request=0xc020462a [nr=0x2a (42), cmd=0x2080014b (545259851)] => ret=0 + ... +Alloc: + Alloc ioctl: request=0xc030462b [nr=0x2b (43), hClass=0xc639 (50745)] => ret=0 + Alloc ioctl: request=0xc030462b [nr=0x2b (43), hClass=0xc640 (50752)] => ret=0 + ... +Unknown: +``` + +Note that by default, `run_sniffer` assumes the shared library is located in the +same directory. You can specify the path to the library with the optional +`-ld_preload` flag. diff --git a/tools/ioctl_sniffer/ioctl.proto b/tools/ioctl_sniffer/ioctl.proto new file mode 100644 index 000000000..8d4b95102 --- /dev/null +++ b/tools/ioctl_sniffer/ioctl.proto @@ -0,0 +1,32 @@ +// Copyright 2024 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. + +syntax = "proto3"; + +package gvisor; + +message Ioctl { + // The path of the file that `fd` is pointing to. + string fd_path = 1; + + // The request argument of the ioctl. + uint64 request = 2; + + // The return value of the ioctl. + int32 ret = 3; + + // The data pointed to by `argp`. For UVM ioctl calls, the argument size is + // not easily accessible, so `arg_data` will be empty in this case. + bytes arg_data = 4; +} diff --git a/tools/ioctl_sniffer/ioctl_hook.cc b/tools/ioctl_sniffer/ioctl_hook.cc new file mode 100644 index 000000000..01e283a99 --- /dev/null +++ b/tools/ioctl_sniffer/ioctl_hook.cc @@ -0,0 +1,90 @@ +// Copyright 2024 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. + +#define _GNU_SOURCE 1 // Needed for access to RTLD_NEXT +#include "tools/ioctl_sniffer/ioctl_hook.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "tools/ioctl_sniffer/ioctl.pb.h" +#include "tools/ioctl_sniffer/sniffer_bridge.h" + +using gvisor::Ioctl; + +libc_ioctl libc_ioctl_handle = nullptr; + +void init_libc_ioctl_handle() { + if (libc_ioctl_handle) { + return; + } + + libc_ioctl_handle = (libc_ioctl)dlsym(RTLD_NEXT, "ioctl"); + if (!libc_ioctl_handle) { + printf("Failed to hook ioctl: %s\n", dlerror()); + exit(1); + } +} + +extern "C" { + +int ioctl(int fd, uint64_t request, void *argp) { + if (!libc_ioctl_handle) { + init_libc_ioctl_handle(); + } + + // Forward the ioctl call. + int ret = libc_ioctl_handle(fd, request, argp); + + // Check the file name to see if this is an Nvidia ioctl. + // We only want to do protobuf logging for these ioctls. + char file_name[PATH_MAX + 1]; + int n = readlink(absl::StrCat("/proc/self/fd/", fd).c_str(), file_name, + sizeof(file_name) - 1); + if (n < 0) { + return ret; + } + + file_name[n] = '\0'; + if (!absl::StartsWith(file_name, "/dev/nvidia")) { + return ret; + } + + // Prepare ioctl proto for logging. + Ioctl info; + info.set_fd_path(file_name); + info.set_request(request); + info.set_ret(ret); + + // ioctl calls to uvm don't encode their size in the request. + uint32_t arg_size = + strcmp(file_name, "/dev/nvidia-uvm") == 0 ? 0 : _IOC_SIZE(request); + info.set_arg_data(argp, arg_size); + + WriteIoctlProto(info); + + return ret; +} + +} // extern "C" diff --git a/tools/ioctl_sniffer/ioctl_hook.h b/tools/ioctl_sniffer/ioctl_hook.h new file mode 100644 index 000000000..35adef6bb --- /dev/null +++ b/tools/ioctl_sniffer/ioctl_hook.h @@ -0,0 +1,24 @@ +// Copyright 2024 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. + +#ifndef TOOLS_IOCTL_SNIFFER_IOCTL_HOOK_H_ +#define TOOLS_IOCTL_SNIFFER_IOCTL_HOOK_H_ + +#include + +typedef int (*libc_ioctl)(int fd, uint64_t request, void *argp); + +void init_libc_ioctl_handle(); + +#endif // TOOLS_IOCTL_SNIFFER_IOCTL_HOOK_H_ diff --git a/tools/ioctl_sniffer/run_sniffer.go b/tools/ioctl_sniffer/run_sniffer.go new file mode 100644 index 000000000..6af7bbb8a --- /dev/null +++ b/tools/ioctl_sniffer/run_sniffer.go @@ -0,0 +1,79 @@ +// Copyright 2024 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. + +// Package main sets up the ioctl sniffer and runs a given command. +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/tools/ioctl_sniffer/sniffer" +) + +var ldPreloadPath = flag.String("ld_preload", "./libioctl_hook.so", "The path to the LD_PRELOAD library.") + +// Main is our main function. +func Main() error { + flag.Parse() + if len(flag.Args()) == 0 { + return fmt.Errorf("no command specified") + } + + // Init our sniffer + if err := sniffer.Init(); err != nil { + return fmt.Errorf("failed to init sniffer: %w", err) + } + + // Create a pipe to read the output of the command. + r, w, err := os.Pipe() + if err != nil { + return fmt.Errorf("failed to create pipe: %w", err) + } + + // Set up command from flags + cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...) + cmd.ExtraFiles = []*os.File{w} + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), fmt.Sprintf("LD_PRELOAD=%s", *ldPreloadPath)) + + // Run the command and start reading the output. + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to run command: %w", err) + } + + w.Close() + results := sniffer.ReadHookOutput(r) + + // Once we've read all the output, print the list of missing ioctls. + log.Infof("%s", results) + + if err := cmd.Wait(); err != nil { + return fmt.Errorf("command exited with error: %w", err) + } + + return nil +} + +func main() { + if err := Main(); err != nil { + log.Warningf("%v", err) + os.Exit(1) + } +} diff --git a/tools/ioctl_sniffer/sniffer/BUILD b/tools/ioctl_sniffer/sniffer/BUILD new file mode 100644 index 000000000..42f736df7 --- /dev/null +++ b/tools/ioctl_sniffer/sniffer/BUILD @@ -0,0 +1,23 @@ +load("//tools:defs.bzl", "go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "sniffer", + srcs = [ + "sniffer.go", + "sniffer_bridge.go", + ], + visibility = ["//tools/ioctl_sniffer:__subpackages__"], + deps = [ + "//pkg/abi/linux", + "//pkg/abi/nvgpu", + "//pkg/log", + "//pkg/sentry/devices/nvproxy", + "//tools/ioctl_sniffer:ioctl_go_proto", + "@org_golang_google_protobuf//proto:go_default_library", + ], +) diff --git a/tools/ioctl_sniffer/sniffer/sniffer.go b/tools/ioctl_sniffer/sniffer/sniffer.go new file mode 100644 index 000000000..259fafc44 --- /dev/null +++ b/tools/ioctl_sniffer/sniffer/sniffer.go @@ -0,0 +1,278 @@ +// Copyright 2024 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. + +// Package sniffer parses the output of the ioctl hook. +package sniffer + +import ( + "errors" + "fmt" + "io" + "regexp" + "strings" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/abi/nvgpu" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/devices/nvproxy" + pb "gvisor.dev/gvisor/tools/ioctl_sniffer/ioctl_go_proto" +) + +var ( + uvmDevPath = "/dev/nvidia-uvm" + ctlDevPath = "/dev/nvidiactl" + deviceDevPath = regexp.MustCompile(`/dev/nvidia(\d+)`) +) + +const ( + frontend = iota + uvm + control + alloc + unknown +) + +// ioctlClass is the class of the ioctl as defined above. +type ioctlClass uint32 + +// ioctlNr is the command number of the ioctl. +type ioctlNr uint32 + +// controlCommand is the control command, specifically for the NV_ESC_RM_CONTROL ioctl. +type controlCommand uint32 + +// allocClass is the alloc class, specifically for the NV_ESC_RM_ALLOC ioctl. +type allocClass uint32 + +func (c ioctlClass) String() string { + switch c { + case frontend: + return "Frontend" + case uvm: + return "UVM" + case control: + return "Control" + case alloc: + return "Alloc" + default: + return "Unknown" + } +} + +var ( + suppFrontendIoctls, suppUvmIoctls, suppControlCmds, suppAllocClasses map[uint32]struct{} +) + +// Ioctl contains the parsed ioctl protobuf information. +type Ioctl struct { + pb *pb.Ioctl + class ioctlClass + nr ioctlNr + cmd controlCommand // Control ioctls only. + hClass allocClass // Alloc ioctls only. +} + +// IsSupported returns true if the ioctl is supported by nvproxy. +func (i Ioctl) IsSupported() bool { + switch i.class { + case frontend: + _, ok := suppFrontendIoctls[uint32(i.nr)] + return ok + case uvm: + _, ok := suppUvmIoctls[uint32(i.nr)] + return ok + case control: + _, ok := suppControlCmds[uint32(i.cmd)] + return ok + case alloc: + _, ok := suppAllocClasses[uint32(i.hClass)] + return ok + default: + return false + } +} + +func (i Ioctl) String() string { + switch i.class { + case control: + return fmt.Sprintf("%s ioctl: request=%#x [nr=%#x (%d), cmd=%#x (%d)] => ret=%d", + i.class, i.pb.GetRequest(), i.nr, i.nr, i.cmd, i.cmd, i.pb.GetRet()) + case alloc: + return fmt.Sprintf("%s ioctl: request=%#x [nr=%#x (%d), hClass=%#x (%d)] => ret=%d", + i.class, i.pb.GetRequest(), i.nr, i.nr, i.hClass, i.hClass, i.pb.GetRet()) + default: + return fmt.Sprintf("%s ioctl: request=%#x [nr=%#x (%d), size=%d] => ret=%d", + i.class, i.pb.GetRequest(), i.nr, i.nr, len(i.pb.GetArgData()), i.pb.GetRet()) + } +} + +// Results contains the list of unsupported ioctls. +type Results struct { + unsupportedControl map[controlCommand]Ioctl + unsupportedAlloc map[allocClass]Ioctl + unsupportedOther map[ioctlClass]map[ioctlNr]Ioctl +} + +// NewResults creates a new Results object. +func NewResults() *Results { + return &Results{ + unsupportedControl: make(map[controlCommand]Ioctl), + unsupportedAlloc: make(map[allocClass]Ioctl), + unsupportedOther: make(map[ioctlClass]map[ioctlNr]Ioctl), + } +} + +// AddUnsupportedIoctl adds an unsupported ioctl to the results. +func (r *Results) AddUnsupportedIoctl(ioctl Ioctl) { + switch ioctl.class { + case control: + r.unsupportedControl[ioctl.cmd] = ioctl + case alloc: + r.unsupportedAlloc[ioctl.hClass] = ioctl + default: + if r.unsupportedOther[ioctl.class] == nil { + r.unsupportedOther[ioctl.class] = make(map[ioctlNr]Ioctl) + } + r.unsupportedOther[ioctl.class][ioctl.nr] = ioctl + } +} + +func printIoctls[T comparable](b *strings.Builder, class ioctlClass, m map[T]Ioctl) { + if len(m) == 0 { + fmt.Fprintf(b, "%v: None\n", class) + return + } + + fmt.Fprintf(b, "%v:\n", class) + for _, ioctl := range m { + fmt.Fprintf(b, "\t%v\n", ioctl) + } +} + +func (r *Results) String() string { + // We will rarely print out the results, so allocating a new strings.Builder + // each time is fine. + b := new(strings.Builder) + + fmt.Fprintln(b, "============== Unsupported ioctls ==============") + printIoctls(b, frontend, r.unsupportedOther[frontend]) + printIoctls(b, uvm, r.unsupportedOther[uvm]) + printIoctls(b, control, r.unsupportedControl) + printIoctls(b, alloc, r.unsupportedAlloc) + printIoctls(b, unknown, r.unsupportedOther[unknown]) + + return b.String() +} + +// Init reads from nvproxy and sets up the supported ioctl maps. +func Init() error { + nvproxy.Init() + + // Load the ABI for the host driver. + driverVerStr, err := nvproxy.HostDriverVersion() + if err != nil { + return fmt.Errorf("failed to get host driver version: %w", err) + } + driverVer, err := nvproxy.DriverVersionFrom(driverVerStr) + if err != nil { + return fmt.Errorf("failed to parse host driver version: %w", err) + } + + log.Infof("Host driver version: %v", driverVer) + + var ok bool + suppFrontendIoctls, suppUvmIoctls, suppControlCmds, suppAllocClasses, ok = nvproxy.SupportedIoctls(driverVer) + if !ok { + return fmt.Errorf("host driver version %s is not supported", driverVer) + } + + return nil +} + +// ReadHookOutput reads the output of the ioctl hook until an EOF is reached. +func ReadHookOutput(r io.Reader) *Results { + res := NewResults() + for { + ioctlPB, err := ReadIoctlProto(r) + if err != nil { + if !errors.Is(err, io.EOF) { + log.Warningf("Error reading ioctl proto: %v", err) + } + break + } + + // Parse the protobuf + ioctl, err := ParseIoctlOutput(ioctlPB) + if err != nil { + log.Warningf("Error parsing ioctl %v: %v", ioctlPB, err) + continue + } + + log.Debugf("%s", ioctl) + + if !ioctl.IsSupported() { + res.AddUnsupportedIoctl(ioctl) + } + } + return res +} + +// ParseIoctlOutput parses an ioctl protobuf from the ioctl hook. +func ParseIoctlOutput(ioctl *pb.Ioctl) (Ioctl, error) { + parsedIoctl := Ioctl{pb: ioctl} + + // Categorize and do class-specific parsing. + switch { + case ioctl.GetFdPath() == uvmDevPath: + parsedIoctl.class = uvm + parsedIoctl.nr = ioctlNr(ioctl.GetRequest()) + case ioctl.GetFdPath() == ctlDevPath || deviceDevPath.MatchString(ioctl.GetFdPath()): + parsedIoctl.nr = ioctlNr(linux.IOC_NR(uint32(ioctl.GetRequest()))) + + switch parsedIoctl.nr { + case nvgpu.NV_ESC_RM_CONTROL: + data := ioctl.GetArgData() + if uint32(len(data)) != nvgpu.SizeofNVOS54Parameters { + return parsedIoctl, fmt.Errorf("unexpected number of bytes") + } + var ioctlParams nvgpu.NVOS54Parameters + ioctlParams.UnmarshalBytes(data) + + parsedIoctl.class = control + parsedIoctl.cmd = controlCommand(ioctlParams.Cmd) + case nvgpu.NV_ESC_RM_ALLOC: + data := ioctl.GetArgData() + var isNVOS64 bool + switch uint32(len(data)) { + case nvgpu.SizeofNVOS21Parameters: + case nvgpu.SizeofNVOS64Parameters: + isNVOS64 = true + default: + return parsedIoctl, fmt.Errorf("unexpected number of bytes") + } + ioctlParams := nvgpu.GetRmAllocParamObj(isNVOS64) + ioctlParams.UnmarshalBytes(data) + + parsedIoctl.class = alloc + parsedIoctl.hClass = allocClass(ioctlParams.GetHClass()) + default: + parsedIoctl.class = frontend + } + default: + parsedIoctl.class = unknown + parsedIoctl.nr = ioctlNr(linux.IOC_NR(uint32(ioctl.GetRequest()))) + } + + return parsedIoctl, nil +} diff --git a/tools/ioctl_sniffer/sniffer/sniffer_bridge.go b/tools/ioctl_sniffer/sniffer/sniffer_bridge.go new file mode 100644 index 000000000..a047c239f --- /dev/null +++ b/tools/ioctl_sniffer/sniffer/sniffer_bridge.go @@ -0,0 +1,60 @@ +// Copyright 2024 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. + +package sniffer + +import ( + "encoding/binary" + "fmt" + "io" + + "google.golang.org/protobuf/proto" + pb "gvisor.dev/gvisor/tools/ioctl_sniffer/ioctl_go_proto" +) + +var ( + protoBytesBuf []byte +) + +// ReadIoctlProto reads a single ioctl proto from the given reader. Our format is: +// - 8 byte little endian uint64 containing the size of the proto. +// - The proto bytes. +// +// This should match the format in sniffer_bridge.h. +func ReadIoctlProto(r io.Reader) (*pb.Ioctl, error) { + // Read next proto from pipe. + var protoSizeBuf [8]byte + if _, err := io.ReadFull(r, protoSizeBuf[:]); err != nil { + return nil, fmt.Errorf("failed to read proto size: %w", err) + } + protoSize := binary.LittleEndian.Uint64(protoSizeBuf[:]) + + // See if we need to reallocate the buffer. + if cap(protoBytesBuf) < int(protoSize) { + protoBytesBuf = make([]byte, protoSize) + } else { + protoBytesBuf = protoBytesBuf[:protoSize] + } + if _, err := io.ReadFull(r, protoBytesBuf); err != nil { + return nil, fmt.Errorf("failed to read proto data: %w", err) + } + + // Unmarshal and parse proto. + ioctl := &pb.Ioctl{} + if err := proto.Unmarshal(protoBytesBuf, ioctl); err != nil { + return nil, fmt.Errorf("failed to unmarshal proto: %w", err) + } + + return ioctl, nil +} diff --git a/tools/ioctl_sniffer/sniffer_bridge.cc b/tools/ioctl_sniffer/sniffer_bridge.cc new file mode 100644 index 000000000..25f6149cc --- /dev/null +++ b/tools/ioctl_sniffer/sniffer_bridge.cc @@ -0,0 +1,33 @@ +// Copyright 2024 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. + +#include "tools/ioctl_sniffer/sniffer_bridge.h" + +#include +#include +#include + +#include "tools/ioctl_sniffer/ioctl.pb.h" +#include "google/protobuf/io/zero_copy_stream_impl.h" + +void WriteIoctlProto(gvisor::Ioctl &ioctl) { + // Write size of the proto message. + uint64_t size = ioctl.ByteSizeLong(); + write(LOG_OUTPUT_FD, &size, sizeof(size)); + + // Write the proto message. + google::protobuf::io::FileOutputStream os(LOG_OUTPUT_FD); + ioctl.SerializeToZeroCopyStream(&os); + os.Flush(); +} diff --git a/tools/ioctl_sniffer/sniffer_bridge.h b/tools/ioctl_sniffer/sniffer_bridge.h new file mode 100644 index 000000000..69459838f --- /dev/null +++ b/tools/ioctl_sniffer/sniffer_bridge.h @@ -0,0 +1,30 @@ +// Copyright 2024 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. + +#ifndef TOOLS_IOCTL_SNIFFER_SNIFFER_BRIDGE_H_ +#define TOOLS_IOCTL_SNIFFER_SNIFFER_BRIDGE_H_ + +#include "tools/ioctl_sniffer/ioctl.pb.h" + +// The file descriptor to write the ioctl data to. Go's os.exec will +// always make this available. +constexpr int LOG_OUTPUT_FD = 3; + +// Write the ioctl proto to the log output file descriptor. Our format is: +// - 8 byte little endian uint64 containing the size of the proto. +// - The proto bytes. +// This should match the format in sniffer_bridge.go. +void WriteIoctlProto(gvisor::Ioctl &ioctl); + +#endif // TOOLS_IOCTL_SNIFFER_SNIFFER_BRIDGE_H_