Skip to content

Commit

Permalink
os: depend on Readlink only when necessary
Browse files Browse the repository at this point in the history
Currently Readlink gets linked into the binary even when Executable is
not needed.

This reduces a simple "os.Stdout.Write([]byte("hello"))" by ~10KiB.

Previously the executable path was read during init time, because
deleting the executable would make "Readlink" return "(deleted)" suffix.
There's probably a slight chance that the init time reading would return
it anyways.

Updates #6853

Change-Id: Ic76190c5b64d9320ceb489cd6a553108614653d1
Reviewed-on: https://go-review.googlesource.com/c/go/+/311790
Run-TryBot: Tobias Klauser <tobias.klauser@gmail.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Trust: Tobias Klauser <tobias.klauser@gmail.com>
  • Loading branch information
egonelbre authored and tklauser committed Apr 22, 2021
1 parent ecfce58 commit cfe5d79
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 8 deletions.
21 changes: 13 additions & 8 deletions src/os/executable_procfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import (
"runtime"
)

// We query the executable path at init time to avoid the problem of
// readlink returns a path appended with " (deleted)" when the original
// binary gets deleted.
var executablePath, executablePathErr = func() (string, error) {
func executable() (string, error) {
var procfn string
switch runtime.GOOS {
default:
Expand All @@ -25,9 +22,17 @@ var executablePath, executablePathErr = func() (string, error) {
case "netbsd":
procfn = "/proc/curproc/exe"
}
return Readlink(procfn)
}()
path, err := Readlink(procfn)

func executable() (string, error) {
return executablePath, executablePathErr
// When the executable has been deleted then Readlink returns a
// path appended with " (deleted)".
return stringsTrimSuffix(path, " (deleted)"), err
}

// stringsTrimSuffix is the same as strings.TrimSuffix.
func stringsTrimSuffix(s, suffix string) string {
if len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix {
return s[:len(s)-len(suffix)]
}
return s
}
65 changes: 65 additions & 0 deletions src/os/executable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,68 @@ func init() {
os.Exit(0)
}
}

func TestExecutableDeleted(t *testing.T) {
testenv.MustHaveExec(t)
switch runtime.GOOS {
case "windows":
t.Skip("windows does not support deleting running binary")
case "openbsd", "freebsd":
t.Skipf("%v does not support reading deleted binary name", runtime.GOOS)
}

dir := t.TempDir()

src := filepath.Join(dir, "testdel.go")
exe := filepath.Join(dir, "testdel.exe")

err := os.WriteFile(src, []byte(testExecutableDeletion), 0666)
if err != nil {
t.Fatal(err)
}

out, err := osexec.Command(testenv.GoToolPath(t), "build", "-o", exe, src).CombinedOutput()
t.Logf("build output:\n%s", out)
if err != nil {
t.Fatal(err)
}

out, err = osexec.Command(exe).CombinedOutput()
t.Logf("exec output:\n%s", out)
if err != nil {
t.Fatal(err)
}
}

const testExecutableDeletion = `package main
import (
"fmt"
"os"
)
func main() {
before, err := os.Executable()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read executable name before deletion: %v\n", err)
os.Exit(1)
}
err = os.Remove(before)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to remove executable: %v\n", err)
os.Exit(1)
}
after, err := os.Executable()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read executable name after deletion: %v\n", err)
os.Exit(1)
}
if before != after {
fmt.Fprintf(os.Stderr, "before and after do not match: %v != %v\n", before, after)
os.Exit(1)
}
}
`

0 comments on commit cfe5d79

Please sign in to comment.