windmill-labs/windmill · error

AssignProcessToJobObject: {e}

Error message

AssignProcessToJobObject: {e}

What it means

The final step of assign_job_object binds the opened child process handle to the job object via AssignProcessToJobObject. On failure the process and job handles are closed and this io::Error is returned. The classic cause is that the child is already part of another job that cannot host nested assignments (pre-Win8 semantics or explicitly configured).

Source

Thrown at backend/windmill-worker/src/common.rs:913

                std::io::ErrorKind::Other,
                format!("SetInformationJobObject: {e}"),
            )
        })?;

        let process_handle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
            .map_err(|e| {
                let _ = windows::Win32::Foundation::CloseHandle(job);
                std::io::Error::new(
                    std::io::ErrorKind::Other,
                    format!("OpenProcess({pid}): {e}"),
                )
            })?;

        let assign_result = AssignProcessToJobObject(job, process_handle);
        let _ = windows::Win32::Foundation::CloseHandle(process_handle);
        assign_result.map_err(|e| {
            let _ = windows::Win32::Foundation::CloseHandle(job);
            std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("AssignProcessToJobObject: {e}"),
            )
        })?;

        Ok(Win32JobHandle(job))
    }
}

pub fn build_command_with_isolation(program: &str, args: &[&str]) -> Command {
    use tokio::process::Command;

    if crate::is_unshare_enabled() {
        if let Some(unshare_path) = crate::UNSHARE_PATH.as_ref() {
            let mut cmd = Command::new(unshare_path);

            let flags = crate::UNSHARE_ISOLATION_FLAGS.as_str();
            for flag in flags.split_whitespace() {

View on GitHub (pinned to e474e8803c)

Solutions

  1. Check the Win32 error code: ERROR_ACCESS_DENIED typically means the process is already in another job.
  2. Run the worker outside the parent job object (change how the service/CI wrapper launches it) or on Windows 8+ where nested jobs are supported.
  3. Verify the job's limit flags don't include JOB_OBJECT_LIMIT_SILENT_BREAKAWAY restrictions incompatible with assignment.
  4. Fallback: accept degraded cleanup (kill only the direct child pid) if nested jobs are impossible in your deployment.
Defensive patterns

Strategy: try-catch

Validate before calling

// Detect a likely parent job before spawning: workers inside CI/service job objects are prone to this failure.
// Check whether the current process is already assigned to a job (QueryInformationJobObject on the enclosing job) when launching under such wrappers.

Try / catch

match assign_job_object(pid, true) {
    Err(e) if e.to_string().contains("AssignProcessToJobObject") => {
        log::warn!("nested job assignment denied: {e}; falling back to direct child kill");
        // degrade: kill only the direct child pid on cleanup
        Ok(None)
    }
    other => other.map(Some),
}

Prevention

When it happens

Trigger: assign_job_object on Windows where the child already belongs to an incompatible job object — e.g. the worker itself runs inside a job (CI runner, service wrapper) on an OS/config that disallows nested job assignment, breaking the kill-on-close isolation for the job's child processes.

Common situations: Worker running under Windows service wrappers, CI agents, or job-based sandboxes (e.g. older Windows or strict job limit configuration) that forbid AssignProcessToJobObject on already-assigned processes.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/95d9cc7870a62464. Report an issue: GitHub.