Skip to content

Data race: for matrix ref: variables leak between concurrent dependency invocations of the same task #2890

Description

@lwintermelon

Description

Summary

When a task that uses for: matrix: ... ref: <var> to read caller-supplied
variables is invoked concurrently by parallel deps, the referenced
variables leak / cross-contaminate between the concurrent invocations. The
matrix expands with the wrong values, and because no error is raised the
result is silently incorrect output.

Minimal reproduction

Taskfile.yml:

version: "3"

tasks:
  build-bins:
    internal: true
    cmds:
      - for:
          matrix:
            GOARCH: { ref: .ARCH_VAR }
            BIN:   { ref: .BINS_VAR }
        cmd: 'sleep 0.2; echo "built GOARCH={{.ITEM.GOARCH}} BIN={{.ITEM.BIN}}"'

  build-amd64:
    cmds:
      - task: build-bins
        vars: { ARCH_VAR: [amd64], BINS_VAR: [a, b, c] }

  build-arm64:
    cmds:
      - task: build-bins
        vars: { ARCH_VAR: [arm64], BINS_VAR: [x, y] }

  default:
    deps: [build-amd64, build-arm64]

It is a race; run it a few times:

for i in $(seq 1 20); do task; done

Expected

Every run produces exactly:

built GOARCH=amd64 BIN=a
built GOARCH=amd64 BIN=b
built GOARCH=amd64 BIN=c
built GOARCH=arm64 BIN=x
built GOARCH=arm64 BIN=y

Actual

Intermittently (~3/20 runs) the variables cross-contaminate. Examples observed:

# amd64's BINs (a,b,c) all got GOARCH=arm64
built GOARCH=arm64 BIN=a
built GOARCH=arm64 BIN=b
built GOARCH=arm64 BIN=c
built GOARCH=arm64 BIN=x
built GOARCH=arm64 BIN=y

# arm64's BINs (x,y) appeared under amd64, each duplicated
built GOARCH=amd64 BIN=x
built GOARCH=amd64 BIN=x
built GOARCH=amd64 BIN=y
built GOARCH=amd64 BIN=y

The ARCH_VAR / BINS_VAR of one concurrent build-bins call leaks into the
other. Which caller "wins" is non-deterministic.

Why this is severe

  • Silent: no error, non-zero exit, or warning — the matrix simply expands
    with wrong values.
  • For a build tool this is near-worst-case: it produces wrong artifacts
    (e.g. a binary built for the wrong architecture and written to the wrong
    path, or a cross-compiler CC swapped between platforms).

Notes

  • Specific to for: matrix: ... ref: (lazy variable resolution driving the
    matrix). The same tasks invoked concurrently but reading a variable via
    direct interpolation {{.ARCH}} in cmd: (resolved at call time) work
    correctly.
  • go-task --version: Task version: 3.51.1+bd79c29a
  • OS: Linux x86-64

Version

3.51.1+bd79c29a

Operating system

all

Experiments Enabled

No response

Example Taskfile

version: "3"

tasks:
  build-bins:
    internal: true
    cmds:
      - for:
          matrix:
            GOARCH: { ref: .ARCH_VAR }
            BIN:   { ref: .BINS_VAR }
        cmd: 'sleep 0.2; echo "built GOARCH={{.ITEM.GOARCH}} BIN={{.ITEM.BIN}}"'

  build-amd64:
    cmds:
      - task: build-bins
        vars: { ARCH_VAR: [amd64], BINS_VAR: [a, b, c] }

  build-arm64:
    cmds:
      - task: build-bins
        vars: { ARCH_VAR: [arm64], BINS_VAR: [x, y] }

  default:
    deps: [build-amd64, build-arm64]

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions