communicator/ssh: don't share rand object to guarantee unique values

Fixes #10463

I'm really surprised this flew under the radar for years...

By having unique PRNGs, the SSH communicator could and would
generate identical ScriptPaths and two provisioners running in parallel
could overwrite each other and execute the same script. This would
happen because they're both seeded by the current time which could
potentially be identical if done in parallel...

Instead, we share the rand now so that the sequence is guaranteed
unique. As an extra measure of robustness, we also multiple by the PID
so that we're also protected against two processes at the same time.
This commit is contained in:
Mitchell Hashimoto 2016-12-06 00:11:32 -08:00
parent af0600815c
commit 4b1d9cfd7d
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
2 changed files with 25 additions and 5 deletions

View File

@ -178,7 +178,6 @@ func (p *ResourceProvisioner) runScripts(
remotePath := comm.ScriptPath() remotePath := comm.ScriptPath()
err = retryFunc(comm.Timeout(), func() error { err = retryFunc(comm.Timeout(), func() error {
if err := comm.UploadScript(remotePath, script); err != nil { if err := comm.UploadScript(remotePath, script); err != nil {
return fmt.Errorf("Failed to upload script: %v", err) return fmt.Errorf("Failed to upload script: %v", err)
} }

View File

@ -14,6 +14,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/hashicorp/terraform/communicator/remote" "github.com/hashicorp/terraform/communicator/remote"
@ -27,6 +28,13 @@ const (
DefaultShebang = "#!/bin/sh\n" DefaultShebang = "#!/bin/sh\n"
) )
// randShared is a global random generator object that is shared.
// This must be shared since it is seeded by the current time and
// creating multiple can result in the same values. By using a shared
// RNG we assure different numbers per call.
var randLock sync.Mutex
var randShared *rand.Rand
// Communicator represents the SSH communicator // Communicator represents the SSH communicator
type Communicator struct { type Communicator struct {
connInfo *connectionInfo connInfo *connectionInfo
@ -34,7 +42,6 @@ type Communicator struct {
config *sshConfig config *sshConfig
conn net.Conn conn net.Conn
address string address string
rand *rand.Rand
} }
type sshConfig struct { type sshConfig struct {
@ -66,11 +73,22 @@ func New(s *terraform.InstanceState) (*Communicator, error) {
return nil, err return nil, err
} }
// Setup the random number generator once. The seed value is the
// time multiplied by the PID. This can overflow the int64 but that
// is okay. We multiply by the PID in case we have multiple processes
// grabbing this at the same time. This is possible with Terraform and
// if we communicate to the same host at the same instance, we could
// overwrite the same files. Multiplying by the PID prevents this.
randLock.Lock()
defer randLock.Unlock()
if randShared == nil {
randShared = rand.New(rand.NewSource(
time.Now().UnixNano() * int64(os.Getpid())))
}
comm := &Communicator{ comm := &Communicator{
connInfo: connInfo, connInfo: connInfo,
config: config, config: config,
// Seed our own rand source so that script paths are not deterministic
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
} }
return comm, nil return comm, nil
@ -186,9 +204,12 @@ func (c *Communicator) Timeout() time.Duration {
// ScriptPath implementation of communicator.Communicator interface // ScriptPath implementation of communicator.Communicator interface
func (c *Communicator) ScriptPath() string { func (c *Communicator) ScriptPath() string {
randLock.Lock()
defer randLock.Unlock()
return strings.Replace( return strings.Replace(
c.connInfo.ScriptPath, "%RAND%", c.connInfo.ScriptPath, "%RAND%",
strconv.FormatInt(int64(c.rand.Int31()), 10), -1) strconv.FormatInt(int64(randShared.Int31()), 10), -1)
} }
// Start implementation of communicator.Communicator interface // Start implementation of communicator.Communicator interface