Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7f0414a
feat(runtime/taint): add runtime taint propagation
eliottness Jul 23, 2026
f9b3baa
feat(injector): add typed value operation join points
eliottness Jul 23, 2026
a4cdc9e
feat(injector): support statement-level taint advice
eliottness Jul 23, 2026
d008dda
test(iast): add patched compiler fixture suite
eliottness Jul 23, 2026
e37e9e6
docs(iast): document patched Go experiment
eliottness Jul 23, 2026
2556db7
test(iast): resolve every not-proven taint case with executing tests
eliottness Jul 27, 2026
e5ed2a5
feat(injector): add method-expression and method-value join points
eliottness Jul 29, 2026
10b8c39
feat(injector): carry byte and rune scalars across calls, maps and ch…
eliottness Jul 29, 2026
034be2f
feat(runtime/taint): add request generations, scalar transfer and rea…
eliottness Jul 29, 2026
94764a1
test(runtime/taint): extend the instrumented e2e matrix to 114 cases
eliottness Jul 29, 2026
4f37b64
feat(iast): advance the patched Go shadow toolchain to v28
eliottness Jul 29, 2026
00efe30
test(iast): reject silent green runs in the patched-Go fixture suite
eliottness Jul 29, 2026
edc906e
docs(iast): correct the coverage ledger and generate the report from it
eliottness Jul 29, 2026
a2861f1
chore(iast): add the missing license header to four shadow fixtures
eliottness Jul 29, 2026
27209c0
feat(iast): add a differences-only filter to the coverage report
eliottness Jul 29, 2026
f2b7f25
test(iast): record what each case observed, not just whether it passed
eliottness Jul 29, 2026
c6835bc
test(runtime/taint): cover the default reporter's value redaction
eliottness Jul 29, 2026
ca39f8e
docs(iast): measure 94 cells that were carrying inherited claims
eliottness Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions experiments/go-shadow/0001-debug-gate.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/src/cmd/compile/internal/base/debug.go b/src/cmd/compile/internal/base/debug.go
index c92df0c9b3..7f8d2c1f45 100644
--- a/src/cmd/compile/internal/base/debug.go
+++ b/src/cmd/compile/internal/base/debug.go
@@ -68,6 +68,7 @@ type DebugFlags struct {
StaticCopy int `help:"print information about missed static copies" concurrent:"ok"`
SyncFrames int `help:"how many writer stack frames to include at sync points in unified export data"`
TailCall int `help:"print information about tail calls"`
+ Taint int `help:"enable experimental IAST shadow taint instrumentation" concurrent:"ok"`
TypeAssert int `help:"print information about type assertion inlining"`
WB int `help:"print information about write barriers"`
ABIWrap int `help:"print information about ABI wrapper generation"`
316 changes: 316 additions & 0 deletions experiments/go-shadow/COVERAGE-LEDGER.md

Large diffs are not rendered by default.

116 changes: 116 additions & 0 deletions experiments/go-shadow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Go compiler shadow-taint experiment

## Consolidated fixture suite

Set `TAINT_GO` to the patched toolchain and run the complete behavior matrix with
one command:

```bash
TAINT_GO=/path/to/go-taint-shadow/bin/go go test -count=1 ./experiments/go-shadow/suite
```

`TAINT_GO`'s compiler must self-identify as the patched build: the suite runs
`go tool compile -V=full` first and fails unless the output contains
`iast-taint-shadow-v28`. A stock toolchain emits no shadow labels, so without that
preflight every zero-report fixture would pass and the run would look green while
proving nothing.

Without `TAINT_GO` the fixture test skips and the package still reports `ok`, so a plain
`go test ./...` covers none of these fixtures. `TestFixtureInventory` runs with the stock
toolchain and independently rejects fixture rot — an empty manifest, a case that omits
`dirtyReports`, a case that overrides `TAINT_PATH` through `env`, or a fixture directory
that contributes no cases at all.

The suite covers enabled and disabled compilation, dirty and clean sinks,
static and dynamic calls, interface dispatch, address-taken parameters, stack
growth, channels and selects, maps, closures, GC address reuse, race builds, and
the zero-sized-channel resource regression. Each program under `fixture/`
remains independently runnable for focused debugging.

This experiment is applied to an isolated worktree of the Go source checkout at
`~/dd/golang/go`. It must not be applied to the primary checkout.

Example isolated worktree:

```text
${TMPDIR}/opencode/go-taint-shadow
```

The first patch adds a rollback-safe compiler gate:

```bash
git apply /path/to/orchestrion/experiments/go-shadow/0001-debug-gate.patch
cd src
./make.bash
../bin/go tool compile -d help
```

`-d=taint=1` will gate all later SSA and runtime modifications. With the flag
disabled, the compiler must remain behaviorally identical to the unmodified
toolchain.

## Verified gate

The patched Go 1.26.1 toolchain built successfully with `src/make.bash`. The
fixture in `fixture/` printed `shadow-gate-ok` both with and without
`-gcflags=all=-d=taint=1`, proving the flag is accepted and currently has no
effect when no shadow pass is installed.

## Implemented shadow protocol

The compiler pairs tracked string SSA values with runtime `uint8` labels. Source,
merge, call, and sink decisions execute in the target program; compile-time taint
decisions and sink diagnostics are intentionally excluded.

In addition to per-value SSA labels, the patch keeps a **byte-precise data
shadow**: each existing arena shadow byte is a bitmask carrying one bit per
application byte (byte precision at no extra memory). Because Go string data is
immutable and shared, the data shadow rides all header copies (map/channel/
struct storage, goroutine arguments, sub-slice results such as `strings.Cut`,
`Split`, `TrimPrefix`, `regexp.FindString`) with no per-operation cost, and it
resolves reflective overwrites correctly (a replaced value has its own clean
backing bytes).

The current patch implements:

- `os.Getenv("TAINT_PATH")` source (gated on the key) and a single `os.OpenFile`
sink (which `os.Open`/`os.Create` funnel through), scanning both the SSA label
and the value's backing bytes;
- SSA aliases, phis, static calls, function values, interface dispatch, recursion,
panic/recover, deferred named results, and address-taken parameters;
- authenticated per-goroutine argument/result transitions;
- byte-precise data-shadow propagation through the runtime string/slice
primitives: `concatstrings*`, `slicebytetostring`, `stringtoslicebyte`,
`slicerunetostring`, `stringtoslicerune`, `growslice`, `slicecopy`, and
`makeslicecopy`;
- compiler routing of `copy()` and slice/string `append` through `slicecopy`,
make+copy through `makeslicecopy`, and `clear()` through a shadow-clear, so
`bytes.Buffer`/`strings.Builder`/`io.Copy`/encoders (`fmt`, `encoding/json`,
`encoding/xml`, `database/sql`, `bufio`) carry taint end-to-end;
- byte-precise indexed loads/stores: a byte read from tracked memory carries its
shadow bit, a byte written to a slice sets or clears the destination bit, and
overwriting a tainted byte with a clean one removes only that byte's taint;
- closure-environment and non-SSA memory labels;
- dense, atomic arena shadows for heap and stack addresses, including stack moves,
stack reuse, sweep cleanup, and exact-address heap reuse;
- buffered, unbuffered, closed, and selected `chan string` operations;
- Swiss-map assignment, lookup, overwrite, range, delete, clear, clone, and growth;
- inert compilation when `-d=taint` is disabled.

The durable isolated-toolchain diff is `go-taint-shadow.patch`. It reverse-applies
cleanly to the live experiment worktree.

## Current boundaries

- A byte or rune scalar that crosses a package boundary as a non-string argument
or return value (e.g. `bytes.Buffer.WriteByte`/`WriteRune`, `lazybuf.append`,
`utf8.DecodeRuneInString`) does not yet carry its shadow: the interprocedural
transition protocol carries a single string value, not scalar params/results.
This is why the byte-at-a-time transforms `path.Clean`/`filepath.Join`,
`strconv.Quote`, and `net/url.QueryEscape` remain conservatively clean.
- Taint does not propagate through scalar arithmetic, so table-driven transforms
whose output bytes are computed rather than copied (`base64` encoding) are
conservatively clean; a value-derived / tainted-index rule is not implemented.
- Arena shadow memory and runtime structure overhead are currently unconditional.
- The implementation is validated on darwin/arm64; 32-bit execution has not been
exercised.
40 changes: 40 additions & 0 deletions experiments/go-shadow/fixture/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Adding a patched-Go taint fixture
Create one directory. Do not edit the suite driver:
```text
fixture/myfixture/
├── main.go
└── cases.json
```
Use `TAINT_PATH` as the source and pass its value to `os.Open` as the sink:
```go
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2023-present Datadog, Inc.

package main

import "os"

func main() {
path := os.Getenv("TAINT_PATH")
_, _ = os.Open(path)
}
```

Declare one or more globally unique test cases in `cases.json`:

```json
[
{
"name": "my fixture",
"taintPath": "/tmp/iast-my-fixture",
"dirtyReports": 1,
"taintEnabled": true,
"race": false,
"env": {"EXAMPLE_MODE": "dirty"}
}
]
```

`dirtyReports` defaults to `0`; `race` and `env` are optional.
8 changes: 8 additions & 0 deletions experiments/go-shadow/fixture/addressparam/cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"name": "address-taken parameter",
"taintPath": "/tmp/iast-param",
"dirtyReports": 1,
"taintEnabled": true
}
]
18 changes: 18 additions & 0 deletions experiments/go-shadow/fixture/addressparam/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2023-present Datadog, Inc.

package main

import "os"

//go:noinline
func hop(value string) string {
pointer := &value
return *pointer
}

func main() {
_, _ = os.Open(hop(os.Getenv("TAINT_PATH")))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"name": "append scalar byte to tainted bytes",
"taintPath": "/tmp/iast-appendscalarbytetotaintedby",
"dirtyReports": 1,
"taintEnabled": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2023-present Datadog, Inc.

package main

import "os"

func main() {
value := []byte(os.Getenv("TAINT_PATH"))
value = append(value, '!')
_, _ = os.Open(string(value))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"name": "append string bytes with source spread",
"taintPath": "/tmp/iast-appendstringbyteswithsource",
"dirtyReports": 1,
"taintEnabled": true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2023-present Datadog, Inc.

package main

import "os"

func main() {
value := append([]byte("prefix-"), os.Getenv("TAINT_PATH")...)
_, _ = os.Open(string(value))
}
20 changes: 20 additions & 0 deletions experiments/go-shadow/fixture/base64encodetostring/cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"name": "base64 encode to string",
"taintPath": "/tmp/iast-base64encodetostring",
"dirtyReports": 1,
"taintEnabled": true
},
{
"name": "base64 encode to string clean",
"taintPath": "",
"dirtyReports": 0,
"taintEnabled": true
},
{
"name": "base64 encode to string disabled",
"taintPath": "/tmp/iast-base64encodetostring-disabled",
"dirtyReports": 0,
"taintEnabled": false
}
]
20 changes: 20 additions & 0 deletions experiments/go-shadow/fixture/base64encodetostring/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2023-present Datadog, Inc.

package main

import (
"encoding/base64"
"os"
)

func main() {
clean := base64.StdEncoding.EncodeToString([]byte("/tmp/iast-base64encodetostring"))
_, _ = os.Open(clean)

source := os.Getenv("TAINT_PATH")
value := base64.StdEncoding.EncodeToString([]byte(source))
_, _ = os.Open(value)
}
20 changes: 20 additions & 0 deletions experiments/go-shadow/fixture/broadcontrolflow/cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"name": "broad-implicit-control-flow",
"taintPath": "secret",
"dirtyReports": 6,
"taintEnabled": true
},
{
"name": "broad-implicit-control-flow-disabled",
"taintPath": "secret",
"dirtyReports": 0,
"taintEnabled": false
},
{
"name": "broad-implicit-control-flow-mismatch",
"taintPath": "other",
"dirtyReports": 3,
"taintEnabled": true
}
]
Loading
Loading