Skip to content

fix: tasks are started with a cwd in a veiled directory - #97

Merged
tgross merged 9 commits into
mainfrom
fix-task-started-cwd-in-veiled-dir
Aug 14, 2026
Merged

fix: tasks are started with a cwd in a veiled directory#97
tgross merged 9 commits into
mainfrom
fix-task-started-cwd-in-veiled-dir

Conversation

@ritesh-harihar

@ritesh-harihar ritesh-harihar commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes: #83

Summary

A task started by exec2 ran with its working directory set to the Nomad client's private internal alloc directory (data/alloc/<id>/<task>) rather than the unveiled bind-mount path ($NOMAD_TASK_DIR). Under Linux Landlock LSM, that private path is not in the unveil list, so any operation that reconstructs the CWD as a path string — pwd, cd $(pwd), relative file writes — either produced the wrong path or failed with Permission denied on hardened systems.

The fix explicitly sets cmd.Dir on the inner task process to $NOMAD_TASK_DIR, matching the behaviour of Nomad's built-in raw_exec and exec drivers. An optional work_dir task config field is also added for consistency with those drivers.

Root Cause

Two-process chain
The exec2 driver runs tasks through a two-process chain. Both processes need their working directory set explicitly:

Nomad agent (root)
  └── nsenter → unshare → exec2-shim          ← outer process (shim.go / prepare())
                                   └── task binary   ← inner process (z_shim_cmd.go)

What was missing

prepare() in shim.go setcmd.Dir = e.env.TaskDir(the private path) for the outer shim process. z_shim_cmd.go built the inner task exec.Command with no cmd.Dir at all — so the task inherited whatever CWD the shim had after Landlock lockdown.

After lockdown() applied Landlock restrictions, the kernel could no longer traverse ancestor directories of the inherited CWD to reconstruct the path string, because those ancestors (data/alloc/…) were not in the unveil list. Only the bind-mount paths ($NOMAD_TASK_DIR, $NOMAD_ALLOC_DIR…) were unveiled.

Additional Improvement — work_dir task config field

As per discussion in the issue, exec2 should be consistent with Nomad's built-in drivers. All of raw_exec, exec, and java expose a work_dir task config field that overrides the default CWD. This PR adds the same.

task "example" {
  driver = "exec2"
  config {
    command  = "/usr/bin/my-service"
    work_dir = "/opt/my-service/data"   # optional; must be absolute
  }
}
Behaviour Default (no work_dir) With work_dir
Task CWD $NOMAD_TASK_DIR The specified absolute path
NOMAD_WORK_DIR env var Set to $NOMAD_TASK_DIR Set to the specified path
Landlock unveil No extra unveil needed Path auto-unveiled with rwxc regardless of unveil_defaults
Relative path n/a Rejected at task start: "work_dir must be an absolute path"

Testing

Details
job "e2job" {
  reschedule {
    attempts  = 0
    unlimited = false
  }
  type = "service"
  group "e2g" {
    count = 1
    task "e2t" {
      restart {
        attempts = 0
      }
      driver = "exec2"
      config {
        command = "${NOMAD_TASK_DIR}/run.sh"
      }
      template {
        destination = "local/run.sh"
        perms       = "0755"
        data        = <<EOD
#!/bin/bash
set -x
exec 2>&1
whoami
env | grep NOMAD_TASK_DIR
env | grep NOMAD_ALLOC_DIR

# This is what pwd reports — should be $NOMAD_TASK_DIR but is the veiled private path
pwd

cd local
pwd

# This is the smoking-gun: cd $(pwd) should be a no-op but fails under Landlock
# because $(pwd) expands to the *private* unveil path not the alloc_mounts path
cd $(pwd)

# Traverse via relative .. — this still works (relative open from an already-open fd)
cd ../secrets
pwd

sleep 1d
EOD
      }

      resources {
        cpu    = 100
        memory = 64
      }
    }
  }
}

Result Before:

riteshharihar@podman-dev:/Users/riteshharihar/Desktop/Src/nomad-driver-exec2$ nomad alloc logs f0d109d5
+ whoami
whoami: failed to get username: No such id: 84269
+ env
+ grep NOMAD_TASK_DIR
NOMAD_TASK_DIR=/tmp/NomadClient3733059173/f0d109d5-5b38-487d-4aa6-a917e5718f8f-e2t/local
+ env
+ grep NOMAD_ALLOC_DIR
NOMAD_ALLOC_DIR=/tmp/NomadClient3733059173/f0d109d5-5b38-487d-4aa6-a917e5718f8f-e2t/alloc
+ pwd
/tmp/NomadClient3733059173/f0d109d5-5b38-487d-4aa6-a917e5718f8f/e2t
+ cd local
+ pwd
/tmp/NomadClient3733059173/f0d109d5-5b38-487d-4aa6-a917e5718f8f/e2t/local
++ pwd
+ cd /tmp/NomadClient3733059173/f0d109d5-5b38-487d-4aa6-a917e5718f8f/e2t/local
+ cd ../secrets
+ pwd
/tmp/NomadClient3733059173/f0d109d5-5b38-487d-4aa6-a917e5718f8f/e2t/secrets
+ sleep 1d

Result After Fix:

riteshharihar@podman-dev:/Users/riteshharihar/Desktop/Src/nomad-driver-exec2$ nomad alloc logs f5615d42
+ whoami
whoami: failed to get username: No such id: 88392
+ env
+ grep NOMAD_TASK_DIR
NOMAD_TASK_DIR=/tmp/NomadClient1676072496/f5615d42-1661-6e69-2782-229fd587e676-e2t/local
+ env
+ grep NOMAD_ALLOC_DIR
NOMAD_ALLOC_DIR=/tmp/NomadClient1676072496/f5615d42-1661-6e69-2782-229fd587e676-e2t/alloc
+ pwd
/tmp/NomadClient1676072496/f5615d42-1661-6e69-2782-229fd587e676-e2t/local
+ cd local
/tmp/NomadClient1676072496/f5615d42-1661-6e69-2782-229fd587e676-e2t/local/run.sh: line 11: cd: local: No such file or directory
+ pwd
/tmp/NomadClient1676072496/f5615d42-1661-6e69-2782-229fd587e676-e2t/local
++ pwd
+ cd /tmp/NomadClient1676072496/f5615d42-1661-6e69-2782-229fd587e676-e2t/local
+ cd ../secrets
+ pwd
/tmp/NomadClient1676072496/f5615d42-1661-6e69-2782-229fd587e676-e2t/secrets
+ sleep 1d
  1. relative file write succeeds
job "relwrite" {
  type = "batch"

  group "group" {
    reschedule {
      attempts  = 0
      unlimited = false
    }

    restart {
      attempts = 0
      mode     = "fail"
    }

    task "task" {
      driver = "exec2"

      config {
        command = "sh"
        args = [
          "-c",
          "echo hello > output.txt && cat output.txt"
        ]
      }

      resources {
        cpu    = 100
        memory = 32
      }
    }
  }
}

Result Berfore:
/usr/bin/sh: 1: cannot create output.txt: Permission denied

Result After Fix:

riteshharihar@podman-dev:/Users/riteshharihar/Desktop/Src/nomad-driver-exec2$ nomad alloc logs 14f62cff
hello
  1. work_dir overrides the default CWD
job "work-dir" {
  type = "batch"

  group "group" {
    reschedule {
      attempts  = 0
      unlimited = false
    }

    restart {
      attempts = 0
      mode     = "fail"
    }

    task "task" {
      driver = "exec2"

      config {
        command = "sh"
        args = [
          "-c",
          "test \"$(pwd)\" = \"$NOMAD_ALLOC_DIR\" && echo WORKDIR_OK"
        ]
        work_dir = "${NOMAD_ALLOC_DIR}"
      }

      resources {
        cpu    = 100
        memory = 32
      }
    }
  }
}

Result:

riteshharihar@podman-dev:/Users/riteshharihar/Desktop/Src/nomad-driver-exec2$ nomad alloc logs e2016d35
WORKDIR_OK
  1. work_dir rejects a relative path
job "work-dir-bad" {
  type = "batch"

  group "group" {
    task "task" {
      driver = "exec2"

      config {
        command = "pwd"
        work_dir = "relative/path"
      }

      resources {
        cpu    = 100
        memory = 32
      }
    }
  }
}

Result:

riteshharihar@podman-dev:/Users/riteshharihar/Desktop/Src/nomad-driver-exec2$ nomad alloc status 86562092
ID                   = 86562092-e86d-3418-5826-5e6fbfca72f5
Eval ID              = 81de2ed5
Name                 = work-dir-bad.group[0]
Node ID              = 616ee344
Node Name            = podman-dev
Job ID               = work-dir-bad
Job Version          = 0
Client Status        = failed
Client Description   = Failed tasks
Desired Status       = stop
Desired Description  = alloc was rescheduled because it failed
Created              = 7s ago
Modified             = 3s ago
Replacement Alloc ID = d831cd60

Task "task" is "dead"
Task Resources:
CPU      Memory  Disk     Addresses
100 MHz  32 MiB  300 MiB  

Task Events:
Started At     = N/A
Finished At    = 2026-08-04T06:52:44Z
Total Restarts = 0
Last Restart   = N/A

Recent Events:
Time                       Type            Description
2026-08-04T12:22:44+05:30  Not Restarting  Error was unrecoverable
2026-08-04T12:22:44+05:30  Driver Failure  rpc error: code = Unknown desc = work_dir must be an absolute path: "relative/path"
2026-08-04T12:22:44+05:30  Task Setup      Building Task Directory
2026-08-04T12:22:44+05:30  Received        Task received by client
  • If a change needs to be reverted, we will roll out an update to the code within 7 days.

Changes to Security Controls

Are there any changes to security controls (access controls, encryption, logging) in this pull request? If so, explain.

@ritesh-harihar ritesh-harihar changed the title fix: Exec2 tasks are started with a cwd in a veiled directory fix: tasks are started with a cwd in a veiled directory Aug 4, 2026
@ritesh-harihar ritesh-harihar linked an issue Aug 4, 2026 that may be closed by this pull request
@ritesh-harihar
ritesh-harihar marked this pull request as ready for review August 5, 2026 05:21
@ritesh-harihar
ritesh-harihar requested a review from a team as a code owner August 5, 2026 05:21
Comment thread pkg/shim/z_shim_cmd.go
// the environment has already been set for us by the exec2 driver;
// NOMAD_WORK_DIR is set to work_dir if configured, otherwise NOMAD_TASK_DIR
cmd := exec.Command(cmdpath, commands[1:]...)
cmd.Dir = os.Getenv("NOMAD_WORK_DIR")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is the main fix.

@ritesh-harihar ritesh-harihar self-assigned this Aug 7, 2026
Comment thread pkg/shim/shim.go
Comment on lines +319 to +324
// set the working directory; defaults to NOMAD_TASK_DIR when not overridden
if workDir != "" {
env["NOMAD_WORK_DIR"] = workDir
} else {
env["NOMAD_WORK_DIR"] = env["NOMAD_TASK_DIR"]
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is fine but I'm beginning to think we have a large set of configuration options we're trying to pass thru the shim. Maybe we should think about generating a config file that the shim loads? That's how runc works.

We'd need to work out how we'd introduce that across task driver version upgrades, but maybe the shim for existing tasks doesn't care?

@ritesh-harihar ritesh-harihar Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Maybe we should think about generating a config file that the shim loads? We'd need to work out how we'd introduce that across task driver version upgrades

Yeah got this. Shall I keep the config-file refactor in this PR, or should I create a separate issue for it? I think it might require some additional effort and testing, so a follow-up might make more sense.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would definitely have that as a follow-up. It's a major architectural change.

Comment thread plugin/driver.go
Comment thread plugin/driver.go Outdated
Comment thread plugin/driver.go Outdated

@tgross tgross left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@tgross
tgross merged commit 8598c33 into main Aug 14, 2026
11 checks passed
@tgross
tgross deleted the fix-task-started-cwd-in-veiled-dir branch August 14, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exec2 tasks are started with a cwd in a veiled directory

2 participants