mirror of https://github.com/status-im/consul.git
Merge pull request #10324 from hashicorp/dnephin/fix-envoy-bootstrap-exec
envoy: fix deadlock when input is larger than named pipe buffer size
This commit is contained in:
parent
cab82ca5a0
commit
07a1b7d081
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
envoy: fixes a bug where a large envoy config could cause the `consul connect envoy` command to deadlock when attempting to start envoy.
|
||||||
|
```
|
|
@ -3,8 +3,10 @@
|
||||||
package envoy
|
package envoy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -189,11 +191,7 @@ func TestHelperProcess(t *testing.T) {
|
||||||
|
|
||||||
limitProcessLifetime(2 * time.Minute)
|
limitProcessLifetime(2 * time.Minute)
|
||||||
|
|
||||||
// This is another level of gross - we are relying on `consul` being on path
|
patchExecArgs(t)
|
||||||
// and being the correct version but in general that is true under `make
|
|
||||||
// test`. We already make the same assumption for API package tests.
|
|
||||||
testSelfExecOverride = "consul"
|
|
||||||
|
|
||||||
err := execEnvoy(
|
err := execEnvoy(
|
||||||
os.Args[0],
|
os.Args[0],
|
||||||
[]string{
|
[]string{
|
||||||
|
@ -264,3 +262,37 @@ func limitProcessLifetime(dur time.Duration) {
|
||||||
os.Exit(99)
|
os.Exit(99)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// patchExecArgs to use a version that will execute the commands using 'go run'.
|
||||||
|
// Also sets up a cleanup function to revert the patch when the test exits.
|
||||||
|
func patchExecArgs(t *testing.T) {
|
||||||
|
orig := execArgs
|
||||||
|
// go run will run the consul source from the root of the repo. The relative
|
||||||
|
// path is necessary because `go test` always sets the working directory to
|
||||||
|
// the directory of the package being tested.
|
||||||
|
execArgs = func(args ...string) (string, []string, error) {
|
||||||
|
args = append([]string{"run", "../../.."}, args...)
|
||||||
|
return "go", args, nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
execArgs = orig
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeBootstrapPipe_DoesNotBlockOnAFullPipe(t *testing.T) {
|
||||||
|
// A named pipe can buffer up to 64k, use a value larger than that
|
||||||
|
bootstrap := bytes.Repeat([]byte("a"), 66000)
|
||||||
|
|
||||||
|
patchExecArgs(t)
|
||||||
|
pipe, err := makeBootstrapPipe(bootstrap)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Read everything from the named pipe, to allow the sub-process to exit
|
||||||
|
f, err := os.Open(pipe)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, err = io.Copy(&buf, f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, bootstrap, buf.Bytes())
|
||||||
|
}
|
||||||
|
|
|
@ -49,6 +49,22 @@ func hasHotRestartOption(argSets ...[]string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execArgs returns the command and args used to execute a binary. By default it
|
||||||
|
// will return a command of os.Executable with the args unmodified. This is a shim
|
||||||
|
// for testing, and can be overridden to execute using 'go run' instead.
|
||||||
|
var execArgs = func(args ...string) (string, []string, error) {
|
||||||
|
execPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(execPath, "/envoy.test") {
|
||||||
|
return "", nil, fmt.Errorf("set execArgs to use 'go run' instead of doing a self-exec")
|
||||||
|
}
|
||||||
|
|
||||||
|
return execPath, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
func makeBootstrapPipe(bootstrapJSON []byte) (string, error) {
|
func makeBootstrapPipe(bootstrapJSON []byte) (string, error) {
|
||||||
pipeFile := filepath.Join(os.TempDir(),
|
pipeFile := filepath.Join(os.TempDir(),
|
||||||
fmt.Sprintf("envoy-%x-bootstrap.json", time.Now().UnixNano()+int64(os.Getpid())))
|
fmt.Sprintf("envoy-%x-bootstrap.json", time.Now().UnixNano()+int64(os.Getpid())))
|
||||||
|
@ -58,33 +74,30 @@ func makeBootstrapPipe(bootstrapJSON []byte) (string, error) {
|
||||||
return pipeFile, err
|
return pipeFile, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get our own executable path.
|
binary, args, err := execArgs("connect", "envoy", "pipe-bootstrap", pipeFile)
|
||||||
execPath, err := os.Executable()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pipeFile, err
|
return pipeFile, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if testSelfExecOverride != "" {
|
|
||||||
execPath = testSelfExecOverride
|
|
||||||
} else if strings.HasSuffix(execPath, "/envoy.test") {
|
|
||||||
return pipeFile, fmt.Errorf("I seem to be running in a test binary without " +
|
|
||||||
"overriding the self-executable. Not doing that - it will make you sad. " +
|
|
||||||
"See testSelfExecOverride.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec the pipe-bootstrap internal sub-command which will write the bootstrap
|
// Exec the pipe-bootstrap internal sub-command which will write the bootstrap
|
||||||
// from STDIN to the named pipe (once Envoy opens it) and then clean up the
|
// from STDIN to the named pipe (once Envoy opens it) and then clean up the
|
||||||
// file for us.
|
// file for us.
|
||||||
cmd := exec.Command(execPath, "connect", "envoy", "pipe-bootstrap", pipeFile)
|
cmd := exec.Command(binary, args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pipeFile, err
|
return pipeFile, err
|
||||||
}
|
}
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return pipeFile, err
|
||||||
|
}
|
||||||
|
|
||||||
// Write the config
|
// Write the config
|
||||||
n, err := stdin.Write(bootstrapJSON)
|
n, err := stdin.Write(bootstrapJSON)
|
||||||
// Close STDIN whether it was successful or not
|
// Close STDIN whether it was successful or not
|
||||||
stdin.Close()
|
_ = stdin.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pipeFile, err
|
return pipeFile, err
|
||||||
}
|
}
|
||||||
|
@ -92,11 +105,6 @@ func makeBootstrapPipe(bootstrapJSON []byte) (string, error) {
|
||||||
return pipeFile, fmt.Errorf("failed writing boostrap to child STDIN: %s", err)
|
return pipeFile, fmt.Errorf("failed writing boostrap to child STDIN: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
return pipeFile, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can't wait for the process since we need to exec into Envoy before it
|
// We can't wait for the process since we need to exec into Envoy before it
|
||||||
// will be able to complete so it will be remain as a zombie until Envoy is
|
// will be able to complete so it will be remain as a zombie until Envoy is
|
||||||
// killed then will be reaped by the init process (pid 0). This is all a bit
|
// killed then will be reaped by the init process (pid 0). This is all a bit
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
package pipebootstrap
|
package pipebootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/command/flags"
|
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/command/flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(ui cli.Ui) *cmd {
|
func New(ui cli.Ui) *cmd {
|
||||||
|
@ -27,20 +28,24 @@ func (c *cmd) init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cmd) Run(args []string) int {
|
func (c *cmd) Run(args []string) int {
|
||||||
|
// Read from STDIN, write to the named pipe provided in the only positional arg
|
||||||
|
if len(args) != 1 {
|
||||||
|
c.UI.Error("Expecting named pipe path as argument")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
// This should never be alive for very long. In case bad things happen and
|
// This should never be alive for very long. In case bad things happen and
|
||||||
// Envoy never starts limit how long we live before just exiting so we can't
|
// Envoy never starts limit how long we live before just exiting so we can't
|
||||||
// accumulate tons of these zombie children.
|
// accumulate tons of these zombie children.
|
||||||
time.AfterFunc(10*time.Second, func() {
|
time.AfterFunc(10*time.Second, func() {
|
||||||
// Force cleanup
|
// Force cleanup
|
||||||
if len(args) > 0 {
|
os.RemoveAll(args[0])
|
||||||
os.RemoveAll(args[0])
|
|
||||||
}
|
|
||||||
os.Exit(99)
|
os.Exit(99)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Read from STDIN, write to the named pipe provided in the only positional arg
|
var buf bytes.Buffer
|
||||||
if len(args) != 1 {
|
if _, err := buf.ReadFrom(os.Stdin); err != nil {
|
||||||
c.UI.Error("Expecting named pipe path as argument")
|
c.UI.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,23 +59,22 @@ func (c *cmd) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(f, os.Stdin)
|
if _, err := buf.WriteTo(f); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.UI.Error(err.Error())
|
c.UI.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f.Close()
|
if err = f.Close(); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.UI.Error(err.Error())
|
c.UI.Error(err.Error())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use Warn to send to stderr, because all logs should go to stderr.
|
||||||
|
c.UI.Warn("Bootstrap sent, unlinking named pipe")
|
||||||
|
|
||||||
// Removed named pipe now we sent it. Even if Envoy has not yet read it, we
|
// Removed named pipe now we sent it. Even if Envoy has not yet read it, we
|
||||||
// know it has opened it and has the file descriptor since our write above
|
// know it has opened it and has the file descriptor since our write above
|
||||||
// will block until there is a reader.
|
// will block until there is a reader.
|
||||||
c.UI.Output("Bootstrap sent, unlinking named pipe")
|
|
||||||
|
|
||||||
os.RemoveAll(args[0])
|
os.RemoveAll(args[0])
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConnectEnvoyPipeBootstrapCommand_noTabs(t *testing.T) {
|
func TestConnectEnvoyPipeBootstrapCommand_noTabs(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
|
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
|
||||||
t.Fatal("help has tabs")
|
t.Fatal("help has tabs")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue