From 9ca09375e4ba1bd9a6b0f8f6c9f57e34563d9f70 Mon Sep 17 00:00:00 2001 From: Etienne Perot Date: Tue, 15 Aug 2023 15:30:07 -0700 Subject: [PATCH] Add tool to generate a Go library that executes an embedded compressed binary. This creates a `go_template` that can generate a `go_library` rule which extracts a compressed binary to a temporary location, then executes it. This is useful to implement `runsc` subcommands without requiring that these subcommands' dependencies are linked into the main `runsc` binary. PiperOrigin-RevId: 557270682 --- tools/embeddedbinary/BUILD | 31 +++++ tools/embeddedbinary/README.md | 30 ++++ tools/embeddedbinary/defs.bzl | 60 ++++++++ .../embeddedbinary/embeddedbinary_template.go | 129 ++++++++++++++++++ tools/embeddedbinary/flatecompress.go | 44 ++++++ tools/embeddedbinary/test/BUILD | 27 ++++ tools/embeddedbinary/test/helloworld/BUILD | 24 ++++ .../test/helloworld/helloworld_bundlee.go | 27 ++++ .../embeddedbinary/test/helloworld_bundler.go | 67 +++++++++ tools/embeddedbinary/test/helloworld_test.go | 49 +++++++ 10 files changed, 488 insertions(+) create mode 100644 tools/embeddedbinary/BUILD create mode 100644 tools/embeddedbinary/README.md create mode 100644 tools/embeddedbinary/defs.bzl create mode 100644 tools/embeddedbinary/embeddedbinary_template.go create mode 100644 tools/embeddedbinary/flatecompress.go create mode 100644 tools/embeddedbinary/test/BUILD create mode 100644 tools/embeddedbinary/test/helloworld/BUILD create mode 100644 tools/embeddedbinary/test/helloworld/helloworld_bundlee.go create mode 100644 tools/embeddedbinary/test/helloworld_bundler.go create mode 100644 tools/embeddedbinary/test/helloworld_test.go diff --git a/tools/embeddedbinary/BUILD b/tools/embeddedbinary/BUILD new file mode 100644 index 000000000..6000c9c2f --- /dev/null +++ b/tools/embeddedbinary/BUILD @@ -0,0 +1,31 @@ +load("//tools:defs.bzl", "bzl_library", "go_binary") +load("//tools/go_generics:defs.bzl", "go_template") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +bzl_library( + name = "defs_bzl", + srcs = ["defs.bzl"], + visibility = [ + "//:sandbox", + ], +) + +go_template( + name = "embeddedbinary_template", + srcs = ["embeddedbinary_template.go"], + visibility = [ + "//:sandbox", + ], +) + +go_binary( + name = "flatecompress", + srcs = ["flatecompress.go"], + visibility = [ + "//:sandbox", + ], +) diff --git a/tools/embeddedbinary/README.md b/tools/embeddedbinary/README.md new file mode 100644 index 000000000..e7193dfd0 --- /dev/null +++ b/tools/embeddedbinary/README.md @@ -0,0 +1,30 @@ +# embeddedbinary + +`embeddedbinary` can embed a binary inside a Go binary, and provides functions +to execute it. + +Embedded binaries are compressed to save on size. They require temporary disk +space to execute, but the disk space is automatically freed when the child +program exits. + +## Usage + +Use the `embedded_binary_go_library` rule defined in `defs.bzl`. + +```build +load(".../defs.bzl", "embedded_binary_go_library") + +# Declare a binary target: +go_binary( + name = "my_binary", + srcs = ["my_binary.go"], +) + +# Generate a go_library rule that can execute the binary target: +embedded_binary_go_library( + name = "my_library", + binary = ":my_binary", +) +``` + +See `test/BUILD` under this directory for a full example. diff --git a/tools/embeddedbinary/defs.bzl b/tools/embeddedbinary/defs.bzl new file mode 100644 index 000000000..3b3499b54 --- /dev/null +++ b/tools/embeddedbinary/defs.bzl @@ -0,0 +1,60 @@ +"""BUILD rule for embedded binaries.""" + +load("//tools:defs.bzl", "go_library") +load("//tools/go_generics:defs.bzl", "go_template_instance") + +_EMBEDDED_BINARY_TEMPLATE = "//tools/embeddedbinary:embeddedbinary_template" +_FLATECOMPRESS = "//tools/embeddedbinary:flatecompress" + +def embedded_binary_go_library( + name, + binary, + binary_name = None, + out = None, + go_package_name = None, + visibility = None): + """Embed a binary and generate a go_library target that can execute it. + + The binary will be compressed, and needs temporary space to be available + when executing. + + Args: + name: The name of the go_library rule. + binary: Binary BUILD target that should be embedded. + binary_name: The name (i.e. typical argv[0]) of the binary being + embedded, defaults to `name`. + out: Output filename of the go_library rule, defaults to `name + ".go"`. + go_package_name: Package name of the go_library, defaults to `name`. + visibility: Visibility of the go_library rule. + """ + if binary_name == None: + binary_name = name + if out == None: + out = name + ".go" + if go_package_name == None: + go_package_name = name + compressed_binary = binary_name + ".flate" + native.genrule( + name = name + "_flate", + outs = [compressed_binary], + cmd = "$(location %s) < $(SRCS) > $(OUTS)" % (_FLATECOMPRESS,), + srcs = [binary], + tools = [_FLATECOMPRESS], + ) + go_template_instance( + name = name + "_lib", + template = _EMBEDDED_BINARY_TEMPLATE, + package = go_package_name, + out = out, + substrs = { + "embedded.bin.name": binary_name, + "//go:embed embedded.bin.flate": "//go:embed %s" % (compressed_binary,), + }, + ) + go_library( + name = name, + srcs = [out], + embedsrcs = [compressed_binary], + deps = ["@org_golang_x_sys//unix:go_default_library"], + visibility = visibility, + ) diff --git a/tools/embeddedbinary/embeddedbinary_template.go b/tools/embeddedbinary/embeddedbinary_template.go new file mode 100644 index 000000000..942272039 --- /dev/null +++ b/tools/embeddedbinary/embeddedbinary_template.go @@ -0,0 +1,129 @@ +// Copyright 2023 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 embeddedbinary embeds an external binary and provides a function to +// exec it. +package embeddedbinary + +import ( + "bytes" + "compress/flate" + _ "embed" + "fmt" + "io" + "os" + "path" + "runtime" + "syscall" + + "golang.org/x/sys/unix" +) + +// BinaryName is the name of the embedded binary. +const BinaryName = "embedded.bin.name" + +//go:embed embedded.bin.flate +var compressedBinary []byte + +// Options is the set of options to execute the embedded binary. +type Options struct { + // Argv is the set of arguments to exec with. + // `Argv[0]` is the name of the binary as invoked. + // If Argv is empty, it will default to a single-element slice, with + // `Argv[0]` being the binary name. + Argv []string + + // Envv is the set of environment variables to pass to the executed process. + Envv []string + + // Files is the set of file descriptors to pass to forked processes. + // Only used when forking, not pure exec'ing. + Files []uintptr +} + +// run decompresses and run the embedded binary with the given arguments. +// If fork is true, the binary runs in a separate process, and its PID is +// returned. +// Otherwise, the binary is exec'd, so the current process stops executing. +func run(options Options, fork bool) (int, error) { + if len(options.Argv) == 0 { + options.Argv = []string{BinaryName} + } + decompressed := flate.NewReader(bytes.NewReader(compressedBinary)) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + myPID := os.Getpid() + oldMask := unix.Umask(0077) + defer unix.Umask(oldMask) + tmpDir, err := os.MkdirTemp("", "gvisor.*.tmp") + if err != nil { + return 0, fmt.Errorf("cannot create temp directory: %w", err) + } + tmpDirHandle, err := os.Open(tmpDir) + if err != nil { + return 0, fmt.Errorf("cannot open temp directory: %w", err) + } + defer tmpDirHandle.Close() + binPath := path.Join(tmpDir, BinaryName) + tmpFile, err := os.OpenFile(binPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0700) + if err != nil { + return 0, fmt.Errorf("cannot open temp file: %w", err) + } + if err := os.RemoveAll(tmpDir); err != nil { + return 0, fmt.Errorf("cannot remove temp directory: %w", err) + } + unix.Umask(oldMask) + if _, err := io.Copy(tmpFile, decompressed); err != nil { + tmpFile.Close() + return 0, fmt.Errorf("cannot decompress embedded binary or write it to temporary file: %w", err) + } + // Reopen the file for reading. + tmpFileReadOnly, err := os.OpenFile(fmt.Sprintf("/proc/%d/fd/%d", myPID, tmpFile.Fd()), os.O_RDONLY, 0700) + if err != nil { + tmpFile.Close() + return 0, fmt.Errorf("cannot re-open temp file for reading: %w", err) + } + if err := tmpFile.Close(); err != nil { + return 0, fmt.Errorf("cannot close temp file: %w", err) + } + defer tmpFileReadOnly.Close() + tmpFD := tmpFileReadOnly.Fd() + if _, err := unix.Seek(int(tmpFD), 0, unix.SEEK_SET); err != nil { + return 0, fmt.Errorf("cannot seek temp file back to 0: %w", err) + } + fdPath := fmt.Sprintf("/proc/%d/fd/%d", myPID, tmpFD) + if fork { + return syscall.ForkExec(fdPath, options.Argv, &syscall.ProcAttr{ + Env: options.Envv, + Files: options.Files, + }) + } + if err := unix.Exec(fdPath, options.Argv, options.Envv); err != nil { + return 0, fmt.Errorf("cannot exec embedded binary: %w", err) + } + panic("unreachable") +} + +// Exec execs the embedded binary. The current process is replaced. +// This function only returns if unsuccessful. +func Exec(options Options) error { + _, err := run(options, false) + return err +} + +// ForkExec runs the embedded binary in a separate process. +// Returns the PID of the child process. +func ForkExec(options Options) (int, error) { + return run(options, true) +} diff --git a/tools/embeddedbinary/flatecompress.go b/tools/embeddedbinary/flatecompress.go new file mode 100644 index 000000000..49d92b1cb --- /dev/null +++ b/tools/embeddedbinary/flatecompress.go @@ -0,0 +1,44 @@ +// Copyright 2023 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. + +// flatecompress compresses data from stdin and writes it to stdout with flate. +package main + +import ( + "compress/flate" + "fmt" + "io" + "os" +) + +func main() { + writer, err := flate.NewWriter(os.Stdout, flate.BestCompression) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot create flate writer: %v\n", err) + os.Exit(1) + } + if _, err := io.Copy(writer, os.Stdin); err != nil { + fmt.Fprintf(os.Stderr, "Failed to compress binary: %v\n", err) + os.Exit(1) + } + if err := writer.Flush(); err != nil { + fmt.Fprintf(os.Stderr, "Cannot flush: %v\n", err) + os.Exit(1) + } + if err := writer.Close(); err != nil { + fmt.Fprintf(os.Stderr, "Cannot close writer: %v\n", err) + os.Exit(1) + } + os.Exit(0) +} diff --git a/tools/embeddedbinary/test/BUILD b/tools/embeddedbinary/test/BUILD new file mode 100644 index 000000000..f6762d01a --- /dev/null +++ b/tools/embeddedbinary/test/BUILD @@ -0,0 +1,27 @@ +load("//tools:defs.bzl", "go_binary", "go_test") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +# helloworld_bundler is a Go program that imports helloworld/helloworld.go and +# calls its functions. Therefore, it should print "Hello, gVisor!\n" to stdout, +# as helloworld/helloworld_bundlee.go does. +go_binary( + name = "helloworld_bundler", + srcs = ["helloworld_bundler.go"], + deps = [ + "//tools/embeddedbinary/test/helloworld", + "@org_golang_x_sys//unix:go_default_library", + ], +) + +# helloworld_test is a test that executes helloworld_bundler as a subprocess. +# It verifies that its output is "Hello, gVisor!\n". +go_test( + name = "helloworld_test", + srcs = ["helloworld_test.go"], + data = [":helloworld_bundler"], + deps = ["//pkg/test/testutil"], +) diff --git a/tools/embeddedbinary/test/helloworld/BUILD b/tools/embeddedbinary/test/helloworld/BUILD new file mode 100644 index 000000000..352c687b7 --- /dev/null +++ b/tools/embeddedbinary/test/helloworld/BUILD @@ -0,0 +1,24 @@ +load("//tools:defs.bzl", "go_binary") +load("//tools/embeddedbinary:defs.bzl", "embedded_binary_go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +# helloworld_bundlee is a simple Go program that prints "Hello, gVisor!\n" +# to stdout. It is bundled by other rules. +go_binary( + name = "helloworld_bundlee", + srcs = ["helloworld_bundlee.go"], +) + +# helloworld generates a Go source file called "helloworld.go" which embeds +# the helloworld_bundlee program. +embedded_binary_go_library( + name = "helloworld", + binary = ":helloworld_bundlee", + visibility = [ + "//tools/embeddedbinary/test:__subpackages__", + ], +) diff --git a/tools/embeddedbinary/test/helloworld/helloworld_bundlee.go b/tools/embeddedbinary/test/helloworld/helloworld_bundlee.go new file mode 100644 index 000000000..95bf1807c --- /dev/null +++ b/tools/embeddedbinary/test/helloworld/helloworld_bundlee.go @@ -0,0 +1,27 @@ +// Copyright 2023 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. + +// helloworld_bundlee writes "Hello, gVisor!\n" to stdout. +// It is meant to be bundled into helloworld_bundler. +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintf(os.Stdout, "Hello, gVisor!\n") + os.Exit(0) +} diff --git a/tools/embeddedbinary/test/helloworld_bundler.go b/tools/embeddedbinary/test/helloworld_bundler.go new file mode 100644 index 000000000..def14fdaa --- /dev/null +++ b/tools/embeddedbinary/test/helloworld_bundler.go @@ -0,0 +1,67 @@ +// Copyright 2023 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. + +// helloworld_bundler bundles helloworld_bundlee and executes it. +package main + +import ( + "fmt" + "os" + + syscall "golang.org/x/sys/unix" + "gvisor.dev/gvisor/tools/embeddedbinary/test/helloworld" +) + +func doExec() { + if err := helloworld.Exec(helloworld.Options{}); err != nil { + fmt.Fprintf(os.Stderr, "Failed to exec embedded binary: %v\n", err) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "Unreachable\n") + os.Exit(1) +} + +func doForkExec() { + childPID, err := helloworld.ForkExec(helloworld.Options{ + // Share stdin/stdout/stderr with child process. + Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to fork+exec embedded binary: %v\n", err) + os.Exit(1) + } + var waitStatus syscall.WaitStatus + if _, err := syscall.Wait4(childPID, &waitStatus, 0, nil); err != nil { + fmt.Fprintf(os.Stderr, "Failed to wait for child embedded binary: %v\n", err) + os.Exit(1) + } + if status := waitStatus.ExitStatus(); status != 0 { + fmt.Fprintf(os.Stderr, "Child embedded binary returned code %d\n", status) + os.Exit(status) + } + os.Exit(0) +} + +func main() { + for _, arg := range os.Args { + switch arg { + case "--mode=exec": + doExec() + case "--mode=fork": + doForkExec() + } + } + fmt.Fprintf(os.Stderr, "Must specify either --mode=exec or --mode=fork.\n") + os.Exit(1) +} diff --git a/tools/embeddedbinary/test/helloworld_test.go b/tools/embeddedbinary/test/helloworld_test.go new file mode 100644 index 000000000..3fccd4e7e --- /dev/null +++ b/tools/embeddedbinary/test/helloworld_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 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 helloworld_test + +import ( + "context" + "fmt" + "os/exec" + "testing" + "time" + + "gvisor.dev/gvisor/pkg/test/testutil" +) + +// TestHelloworld executes helloworld_bundler and verifies that its output +// matches "Hello, gVisor!\n". +func TestHelloworld(t *testing.T) { + ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer ctxCancel() + helloWorldPath, err := testutil.FindFile("tools/embeddedbinary/test/helloworld_bundler") + if err != nil { + t.Fatalf("Cannot find helloworld_bundler path: %v", err) + } + for _, mode := range []string{"exec", "fork"} { + t.Run(mode, func(t *testing.T) { + output, err := exec.CommandContext(ctx, helloWorldPath, fmt.Sprintf("--mode=%s", mode)).CombinedOutput() + outputStr := string(output) + if err != nil { + t.Fatalf("Failed to execute helloworld_bundler: %v; output:\n%v\n", err, outputStr) + } + want := "Hello, gVisor!\n" + if outputStr != want { + t.Fatalf("helloworld_bundler: got output %q want %q", outputStr, want) + } + }) + } +}