Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

job, task group, and task IDs should not allow null characters #9020

Merged
merged 4 commits into from
Oct 5, 2020
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ IMPROVEMENTS:
* jobspec: Lowered minimum CPU allowed from 10 to 1. [[GH-8996](https://github.com/hashicorp/nomad/issues/8996)]

__BACKWARDS INCOMPATIBILITIES:__
* core: null characters are prohibited in region, datacenter, job name/ID, task group name, and task name [[GH-9020](https://github.com/hashicorp/nomad/issues/9020)]
* driver/docker: Tasks are now issued SIGTERM instead of SIGINT when stopping [[GH-8932](https://github.com/hashicorp/nomad/issues/8932)]

BUG FIXES:
Expand Down
10 changes: 5 additions & 5 deletions api/jobs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,7 @@ func TestJobs_Info(t *testing.T) {

// Trying to retrieve a job by ID before it exists
// returns an error
id := "job-id/with\\troublesome:characters\n?&字\000"
id := "job-id/with\\troublesome:characters\n?&字"
_, _, err := jobs.Info(id, nil)
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("expected not found error, got: %#v", err)
Expand Down Expand Up @@ -1993,7 +1993,7 @@ func TestJobs_ScaleAction(t *testing.T) {
defer s.Stop()
jobs := c.Jobs()

id := "job-id/with\\troublesome:characters\n?&字\000"
id := "job-id/with\\troublesome:characters\n?&字"
job := testJobWithScalingPolicy()
job.ID = &id
groupName := *job.TaskGroups[0].Name
Expand Down Expand Up @@ -2054,7 +2054,7 @@ func TestJobs_ScaleAction_Error(t *testing.T) {
defer s.Stop()
jobs := c.Jobs()

id := "job-id/with\\troublesome:characters\n?&字\000"
id := "job-id/with\\troublesome:characters\n?&字"
job := testJobWithScalingPolicy()
job.ID = &id
groupName := *job.TaskGroups[0].Name
Expand Down Expand Up @@ -2106,7 +2106,7 @@ func TestJobs_ScaleAction_Noop(t *testing.T) {
defer s.Stop()
jobs := c.Jobs()

id := "job-id/with\\troublesome:characters\n?&字\000"
id := "job-id/with\\troublesome:characters\n?&字"
job := testJobWithScalingPolicy()
job.ID = &id
groupName := *job.TaskGroups[0].Name
Expand Down Expand Up @@ -2161,7 +2161,7 @@ func TestJobs_ScaleStatus(t *testing.T) {
jobs := c.Jobs()

// Trying to retrieve a status before it exists returns an error
id := "job-id/with\\troublesome:characters\n?&字\000"
id := "job-id/with\\troublesome:characters\n?&字"
_, _, err := jobs.ScaleStatus(id, nil)
require.Error(err)
require.Contains(err.Error(), "not found")
Expand Down
12 changes: 12 additions & 0 deletions command/agent/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,18 @@ func (c *Command) isValidConfig(config, cmdConfig *Config) bool {
return false
}

// Check that the region does not contain invalid characters
if strings.ContainsAny(config.Region, "\000") {
c.Ui.Error("Region contains invalid characters")
return false
}

// Check that the datacenter name does not contain invalid characters
if strings.ContainsAny(config.Datacenter, "\000") {
c.Ui.Error("Datacenter contains invalid characters")
return false
}

// Set up the TLS configuration properly if we have one.
// XXX chelseakomlo: set up a TLSConfig New method which would wrap
// constructor-type actions like this.
Expand Down
101 changes: 100 additions & 1 deletion command/agent/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
"strings"
"testing"

"github.com/hashicorp/nomad/version"
"github.com/mitchellh/cli"

"github.com/hashicorp/nomad/version"
)

func TestCommand_Implements(t *testing.T) {
Expand Down Expand Up @@ -142,3 +143,101 @@ func TestCommand_MetaConfigValidation(t *testing.T) {
}
}
}

func TestCommand_NullCharInDatacenter(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "nomad")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(tmpDir)

tcases := []string{
"char-\\000-in-the-middle",
"ends-with-\\000",
"\\000-at-the-beginning",
}
for _, tc := range tcases {
configFile := filepath.Join(tmpDir, "conf1.hcl")
err = ioutil.WriteFile(configFile, []byte(`
datacenter = "`+tc+`"
client{
enabled = true
}`), 0600)
if err != nil {
t.Fatalf("err: %s", err)
}

// Make a new command. We preemptively close the shutdownCh
// so that the command exits immediately instead of blocking.
ui := cli.NewMockUi()
shutdownCh := make(chan struct{})
close(shutdownCh)
cmd := &Command{
Version: version.GetVersion(),
Ui: ui,
ShutdownCh: shutdownCh,
}

// To prevent test failures on hosts whose hostname resolves to
// a loopback address, we must append a bind address
args := []string{"-client", "-data-dir=" + tmpDir, "-config=" + configFile, "-bind=169.254.0.1"}
if code := cmd.Run(args); code != 1 {
t.Fatalf("args: %v\nexit: %d\n", args, code)
}

out := ui.ErrorWriter.String()
exp := "Datacenter contains invalid characters"
if !strings.Contains(out, exp) {
t.Fatalf("expect to find %q\n\n%s", exp, out)
}
}
}

func TestCommand_NullCharInRegion(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "nomad")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(tmpDir)

tcases := []string{
"char-\\000-in-the-middle",
"ends-with-\\000",
"\\000-at-the-beginning",
}
for _, tc := range tcases {
configFile := filepath.Join(tmpDir, "conf1.hcl")
err = ioutil.WriteFile(configFile, []byte(`
region = "`+tc+`"
client{
enabled = true
}`), 0600)
if err != nil {
t.Fatalf("err: %s", err)
}

// Make a new command. We preemptively close the shutdownCh
// so that the command exits immediately instead of blocking.
ui := cli.NewMockUi()
shutdownCh := make(chan struct{})
close(shutdownCh)
cmd := &Command{
Version: version.GetVersion(),
Ui: ui,
ShutdownCh: shutdownCh,
}

// To prevent test failures on hosts whose hostname resolves to
// a loopback address, we must append a bind address
args := []string{"-client", "-data-dir=" + tmpDir, "-config=" + configFile, "-bind=169.254.0.1"}
if code := cmd.Run(args); code != 1 {
t.Fatalf("args: %v\nexit: %d\n", args, code)
}

out := ui.ErrorWriter.String()
exp := "Region contains invalid characters"
if !strings.Contains(out, exp) {
t.Fatalf("expect to find %q\n\n%s", exp, out)
}
}
}
8 changes: 8 additions & 0 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3943,9 +3943,13 @@ func (j *Job) Validate() error {
mErr.Errors = append(mErr.Errors, errors.New("Missing job ID"))
} else if strings.Contains(j.ID, " ") {
mErr.Errors = append(mErr.Errors, errors.New("Job ID contains a space"))
} else if strings.Contains(j.ID, "\000") {
mErr.Errors = append(mErr.Errors, errors.New("Job ID contains a null chararacter"))
}
if j.Name == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't come up except when using the API, but Jobs have both a name and an ID. Should we be validating that Job.Name also doesn't contain a null char?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, good point. i don't see any indexing there, but a quick test showed that null in Name put a docker job into a failing state:

2020-10-05T12:55:36Z  Driver Failure  Failed to start container cc77616db08d0701b20b438af789855e56eb343f7ce9c0636c779e399e052177: API error (500): OCI runtime create failed: container_linux.go:349: starting container process caused "process_linux.go:449: container init caused \"setenv: invalid argument\"": unknown

will update PR to include Job.Name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh, in addition, the task drivers set the NOMAD_DC environment variable, which will fail if the node datacenter includes a null character 🤦

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and region 🤦++

mErr.Errors = append(mErr.Errors, errors.New("Missing job name"))
} else if strings.Contains(j.Name, "\000") {
mErr.Errors = append(mErr.Errors, errors.New("Job Name contains a null chararacter"))
}
if j.Namespace == "" {
mErr.Errors = append(mErr.Errors, errors.New("Job must be in a namespace"))
Expand Down Expand Up @@ -5740,6 +5744,8 @@ func (tg *TaskGroup) Validate(j *Job) error {
var mErr multierror.Error
if tg.Name == "" {
mErr.Errors = append(mErr.Errors, errors.New("Missing task group name"))
} else if strings.Contains(tg.Name, "\000") {
mErr.Errors = append(mErr.Errors, errors.New("Task group name contains null character"))
}
if tg.Count < 0 {
mErr.Errors = append(mErr.Errors, errors.New("Task group count can't be negative"))
Expand Down Expand Up @@ -6462,6 +6468,8 @@ func (t *Task) Validate(ephemeralDisk *EphemeralDisk, jobType string, tgServices
// We enforce this so that when creating the directory on disk it will
// not have any slashes.
mErr.Errors = append(mErr.Errors, errors.New("Task name cannot include slashes"))
} else if strings.Contains(t.Name, "\000") {
mErr.Errors = append(mErr.Errors, errors.New("Task name cannot include null characters"))
}
if t.Driver == "" {
mErr.Errors = append(mErr.Errors, errors.New("Missing task driver"))
Expand Down
24 changes: 24 additions & 0 deletions nomad/structs/structs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ func TestJob_ValidateScaling(t *testing.T) {
require.Contains(mErr.Errors[0].Error(), "task group count must not be less than minimum count in scaling policy")
}

func TestJob_ValidateNullChar(t *testing.T) {
assert := assert.New(t)

// job id should not allow null characters
job := testJob()
job.ID = "id_with\000null_character"
assert.Error(job.Validate(), "null character in job ID should not validate")

// job name should not allow null characters
job.ID = "happy_little_job_id"
job.Name = "my job name with \000 characters"
assert.Error(job.Validate(), "null character in job name should not validate")

// task group name should not allow null characters
job.Name = "my job"
job.TaskGroups[0].Name = "oh_no_another_\000_char"
assert.Error(job.Validate(), "null character in task group name should not validate")

// task name should not allow null characters
job.TaskGroups[0].Name = "so_much_better"
job.TaskGroups[0].Tasks[0].Name = "ive_had_it_with_these_\000_chars_in_these_names"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

assert.Error(job.Validate(), "null character in task name should not validate")
}

func TestJob_Warnings(t *testing.T) {
cases := []struct {
Name string
Expand Down
8 changes: 8 additions & 0 deletions website/pages/docs/upgrade/upgrade-specific.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ When stopping tasks running with the Docker task driver, Nomad documents that a
versions of Nomad would issue `SIGINT` instead. Starting again with Nomad v0.13.0
`SIGTERM` will be sent by default when stopping Docker tasks.

### Null characters in region, datacenter, job name/ID, task group name, and task names

Starting with Nomad v0.13.0, jobs will fail validation if any of the following
contain null character: the job ID or name, the task group name, or the task name. Any
jobs meeting this requirement should be modified before an update to v0.13.0. Similarly,
client and server config validation will prohibit either the region or the datacenter
from containing null characters.

## Nomad 0.12.0

### `mbits` and Task Network Resource deprecation
Expand Down