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
53 changes: 21 additions & 32 deletions cmd/gomodjail/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/AkihiroSuda/gomodjail/cmd/gomodjail/commands/pack"
"github.com/AkihiroSuda/gomodjail/cmd/gomodjail/commands/run"
"github.com/AkihiroSuda/gomodjail/cmd/gomodjail/version"
"github.com/AkihiroSuda/gomodjail/pkg/cache"
"github.com/AkihiroSuda/gomodjail/pkg/env"
"github.com/AkihiroSuda/gomodjail/pkg/envutil"
"github.com/AkihiroSuda/gomodjail/pkg/osargs"
Expand All @@ -24,19 +25,12 @@ import (
var logLevel = new(slog.LevelVar)

func main() {
exitCode, closer := xmain()
if closer != nil {
if cErr := closer(); cErr != nil {
slog.Error("failed to call closer", "error", cErr)
}
}
if exitCode != 0 {
if exitCode := xmain(); exitCode != 0 {
os.Exit(exitCode)
}
}

func xmain() (int, func() error) {
var closer func() error
func xmain() int {
logHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})
slog.SetDefault(slog.New(logHandler))
rootCmd := newRootCommand()
Expand All @@ -46,14 +40,13 @@ func xmain() (int, func() error) {
slog.Error("error while detecting self-extract archive", "error", err)
}
if zr != nil {
var err error
err, closer = configureSelfExtractMode(rootCmd, zr)
err := configureSelfExtractMode(rootCmd, zr)
if cErr := zr.Close(); cErr != nil {
slog.Error("failed to call closer", "error", cErr)
}
if err != nil {
slog.Error("exiting with an error while setting up self-extract mode", "error", err)
return 1, closer
return 1
}
}
}
Expand All @@ -72,9 +65,9 @@ func xmain() (int, func() error) {
} else {
slog.Debug("exiting")
}
return exitCode, closer
return exitCode
}
return 0, closer
return 0
}

func newRootCommand() *cobra.Command {
Expand Down Expand Up @@ -108,58 +101,54 @@ func newRootCommand() *cobra.Command {
return cmd
}

//nolint:staticcheck // ST1008: error should be returned as the last argument
func configureSelfExtractMode(rootCmd *cobra.Command, zr *zip.ReadCloser) (error, func() error) {
func configureSelfExtractMode(rootCmd *cobra.Command, zr *zip.ReadCloser) error {
slog.Debug("Running in self-extract mode")

td, err := os.MkdirTemp("", "gomodjail-*")
// td must be kept on exit
// https://github.com/containerd/nerdctl/pull/4012#issuecomment-2840539282
td, err := cache.ExecutableDir()
if err != nil {
return err, nil
return err
}
slog.Debug("created self-extract dir", "path", td)
closer := func() error {
slog.Debug("removing self-extract dir", "path", td)
return os.RemoveAll(td)
}

slog.Debug("unpacking self-extract archive", "dir", td)
fis, err := ziputil.Unzip(td, zr)
if err != nil {
return fmt.Errorf("failed to unzip to %q: %w", td, err), closer
return fmt.Errorf("failed to unzip to %q: %w", td, err)
}
var libgomodjailHookFI, progFI, goModFI fs.FileInfo
switch runtime.GOOS {
case "darwin":
if len(fis) != 3 {
return fmt.Errorf("expected an archive to contain 3 files (libgomodjail_hook_darwin.dylib, program and go.mod), got %d files", len(fis)), closer
return fmt.Errorf("expected an archive to contain 3 files (libgomodjail_hook_darwin.dylib, program and go.mod), got %d files", len(fis))
}
libgomodjailHookFI, progFI, goModFI = fis[0], fis[1], fis[2]
default:
if len(fis) != 2 {
return fmt.Errorf("expected an archive to contain 2 files (program and go.mod), got %d files", len(fis)), closer
return fmt.Errorf("expected an archive to contain 2 files (program and go.mod), got %d files", len(fis))
}
progFI, goModFI = fis[0], fis[1]
}
if filepath.Base(progFI.Name()) != progFI.Name() {
return fmt.Errorf("unexpected file name: %q", progFI.Name()), closer
return fmt.Errorf("unexpected file name: %q", progFI.Name())
}
if goModFI.Name() != "go.mod" {
return fmt.Errorf("expected \"go.mod\", got %q", goModFI.Name()), closer
return fmt.Errorf("expected \"go.mod\", got %q", goModFI.Name())
}
prog := filepath.Join(td, progFI.Name())
goMod := filepath.Join(td, goModFI.Name())
switch runtime.GOOS {
case "darwin":
if libgomodjailHookFI.Name() != "libgomodjail_hook_darwin.dylib" {
return fmt.Errorf("expected \"libgomodjail_hook_darwin.dylib\", got %q", libgomodjailHookFI.Name()), closer
return fmt.Errorf("expected \"libgomodjail_hook_darwin.dylib\", got %q", libgomodjailHookFI.Name())
}
libgomodjailHook := filepath.Join(td, libgomodjailHookFI.Name())
if err = os.Setenv("LIBGOMODJAIL_HOOK", libgomodjailHook); err != nil {
return err, closer
return err
}
}
args := append([]string{os.Args[0], "run", "--go-mod=" + goMod, prog, "--"}, os.Args[1:]...)
slog.Debug("Reconfiguring the top-level command", "args", args)
rootCmd.SetArgs(args[1:])
osargs.SetOSArgs(args)
return nil, closer
return nil
}
77 changes: 77 additions & 0 deletions pkg/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cache

import (
"crypto/sha256"
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"

"github.com/AkihiroSuda/gomodjail/pkg/env"
)

func ExecutableDir() (string, error) {
selfPath, err := os.Executable()
if err != nil {
return "", err
}

cacheHome, err := Home()
if err != nil {
return "", fmt.Errorf("failed to resolve GOMODJAIL_CACHE_HOME: %w", err)
}

selfPathDigest := sha256sum([]byte(selfPath))
selfPathDigestPartial := selfPathDigest[0:16]

dir := filepath.Join(cacheHome, selfPathDigestPartial)
return dir, nil
}

func sha256sum(b []byte) string {
h := sha256.New()
if _, err := h.Write(b); err != nil {
panic(err)
}
return fmt.Sprintf("%x", h.Sum(nil))
}

// Home candidates are:
// - $GOMODJAIL_CACHE_HOME
// - $XDG_RUNTIME_DIR/gomodjail
// - $TMPDIR/gomodjail (macOS)
// - $XDG_CACHE_HOME/gomodjail
func Home() (string, error) {
if cacheHome := os.Getenv(env.CacheHome); cacheHome != "" {
return cacheHome, nil
}
if xrd := xdgRuntimeDir(); xrd != "" {
cacheHome := filepath.Join(xrd, "gomodjail")
return cacheHome, nil
}
if runtime.GOOS == "darwin" {
// macOS allocates a unique TMPDIR per user
if td := os.Getenv("TMPDIR"); td != "" {
cacheHome := filepath.Join(td, "gomodjail")
return cacheHome, nil
}
}
osCacheHome, err := os.UserCacheDir()
if err != nil {
return "", err
}
cacheHome := filepath.Join(osCacheHome, "gomodjail")
return cacheHome, nil
}

func xdgRuntimeDir() string {
if xrd := os.Getenv("XDG_RUNTIME_DIR"); xrd != "" {
return xrd
}
xrd := filepath.Join("run", "user", strconv.Itoa(os.Geteuid()))
if _, err := os.Stat(xrd); err == nil {
return xrd
}
return ""
}
5 changes: 4 additions & 1 deletion pkg/env/env.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package env

const PrivateChild = "_GOMODJAIL_PRIVATE_CHILD" // no value
const (
PrivateChild = "_GOMODJAIL_PRIVATE_CHILD" // no value
CacheHome = "GOMODJAIL_CACHE_HOME"
)
30 changes: 27 additions & 3 deletions pkg/ziputil/ziputil.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"io/fs"
"log/slog"
"math"
"os"
"path/filepath"
Expand Down Expand Up @@ -70,6 +71,13 @@ func FindSelfExtractArchive() (*zip.ReadCloser, error) {
}

func Unzip(dir string, zr *zip.ReadCloser) ([]fs.FileInfo, error) {
// https://specifications.freedesktop.org/basedir-spec/latest/#variables
// To ensure that your files are not removed, they should have their access time timestamp modified at least once every 6 hours of monotonic time
// or the 'sticky' bit should be set on the file.
if err := os.MkdirAll(dir, 0o755|os.ModeSticky); err != nil {
return nil, err
}

res := make([]fs.FileInfo, len(zr.File))
for i, f := range zr.File {
if err := unzip1(dir, f); err != nil {
Expand All @@ -96,7 +104,18 @@ func unzip1(dir string, f *zip.File) error {
return fmt.Errorf("unexpected file: %q", fs.FormatFileInfo(fi))
}
wPath := filepath.Join(dir, baseName)
w, err := os.OpenFile(wPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
modTime := fi.ModTime()
if st, err := os.Stat(wPath); err == nil && st.ModTime().Equal(modTime) && modTime.UnixNano() != 0 {
// TODO: compare digest too (via xattr? fs-verity?)
slog.Debug("already exists", "path", wPath, "modTime", modTime)
return nil
}

// for atomicity
wPathTmp := fmt.Sprintf("%s.pid-%d", wPath, os.Getpid())
defer os.RemoveAll(wPathTmp) //nolint:errcheck

w, err := os.OpenFile(wPathTmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()|os.ModeSticky)
if err != nil {
return err
}
Expand All @@ -109,8 +128,13 @@ func unzip1(dir string, f *zip.File) error {
return err
}
}
modTime := fi.ModTime()
if err = os.Chtimes(wPath, modTime, modTime); err != nil {
if err = os.Chtimes(wPathTmp, modTime, modTime); err != nil {
return err
}
if err = os.RemoveAll(wPath); err != nil {
return err
}
if err = os.Rename(wPathTmp, wPath); err != nil {
return err
}
return nil
Expand Down