From 7e069093d08323d8c01ba5864119339cd2e0bfc8 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Sat, 25 Apr 2026 13:40:54 -0230 Subject: [PATCH 1/3] Add a basic fuzz test to ensure output is UTF-8 --- .github/workflows/test.yml | 38 ++++++++++++++++++++++++++++++++++++++ templatefile_test.go | 19 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c79913f..7537717 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,6 +76,43 @@ jobs: - name: go test run: | go test + fuzz: + name: 10s fuzz test + runs-on: ubuntu-24.04 + permissions: + contents: read # Required to clone the repo + steps: + - name: Harden the runner + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + - name: go fuzz + run: | + go test -fuzz=FuzzTemplatefile -fuzztime=10s + - name: Collect any new fuzz corpora + id: corpus + if: failure() + run: | + { + printf '%s\n' 'paths<> "$GITHUB_OUTPUT" + - name: Upload fuzz failure corpora + if: failure() && steps.corpus.outputs.paths != '' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: fuzz-failure-corpus + path: ${{ steps.corpus.outputs.paths }} + retention-days: 14 no-more-jobs: name: No more jobs runs-on: ubuntu-24.04 @@ -83,6 +120,7 @@ jobs: - govulncheck - pinact-verify - fmt-test + - fuzz steps: - name: Harden the runner uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 diff --git a/templatefile_test.go b/templatefile_test.go index 07f1f45..f6f8249 100644 --- a/templatefile_test.go +++ b/templatefile_test.go @@ -2,9 +2,11 @@ package main import ( "io/ioutil" + "os" "path/filepath" "strings" "testing" + "unicode/utf8" "github.com/stretchr/testify/require" ) @@ -33,3 +35,20 @@ func TestTemplatefileRecursive(t *testing.T) { require.NotNil(t, err) require.Contains(t, err.Error(), "Call to unknown function") } + +func FuzzTemplatefile(f *testing.F) { + f.Add("Hello, ${name}!", "name: world\n") + f.Add("${val}", "val: works\n") + f.Add(`The items are ${join(", ", list)}`, "list:\n - foo\n - bar\n") + f.Add("%{ for x in list ~}\n- ${x}\n%{ endfor ~}", "list:\n - foo\n") + + f.Fuzz(func(t *testing.T, tmpl, vars string) { + tmp := t.TempDir() + path := filepath.Join(tmp, "fuzz.tmpl") + require.NoError(t, os.WriteFile(path, []byte(tmpl), 0o600)) + + // Assert that the parser doesn't panic or hang, and produces valid UTF-8 + actual, err := templatefile(tmp, path, vars) + require.True(t, err != nil || utf8.ValidString(actual), "output is invalid UTF-8") + }) +} From 17850f150237f6063cbccb479a633cf12959ddfd Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Sat, 25 Apr 2026 14:20:21 -0230 Subject: [PATCH 2/3] Allow fuzz job to fail --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7537717..4958aae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,6 +79,7 @@ jobs: fuzz: name: 10s fuzz test runs-on: ubuntu-24.04 + continue-on-error: true permissions: contents: read # Required to clone the repo steps: From 74f7ba8a5491394e47bc7ed020cae85fb32f370e Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Sat, 25 Apr 2026 14:29:31 -0230 Subject: [PATCH 3/3] Handle res when it is not a string Found via fuzz testing: fuzz: elapsed: 0s, gathering baseline coverage: 0/4 completed fuzz: elapsed: 0s, gathering baseline coverage: 4/4 completed, now fuzzing with 4 workers fuzz: elapsed: 1s, execs: 1955 (1385/sec), new interesting: 6 (total: 10) --- FAIL: FuzzTemplatefile (1.41s) --- FAIL: FuzzTemplatefile (0.00s) testing.go:1825: panic: not a string goroutine 1245 [running]: runtime/debug.Stack() /opt/hostedtoolcache/go/1.25.9/x64/src/runtime/debug/stack.go:26 +0x9b testing.tRunner.func1() /opt/hostedtoolcache/go/1.25.9/x64/src/testing/testing.go:1825 +0x1d0 panic({0x9ed640?, 0xb16cb0?}) /opt/hostedtoolcache/go/1.25.9/x64/src/runtime/panic.go:783 +0x132 github.com/zclconf/go-cty/cty.Value.AsString({{{0xb1be40?, 0xebf8e0?}}, {0x0?, 0x0?}}) /home/runner/go/pkg/mod/github.com/zclconf/go-cty@v1.18.1/cty/value_ops.go:1458 +0x218 github.com/TypedSoftware/templatefile.templatefile({0xc0053aebd0, 0x23}, {0xc0053aec30, 0x2d}, {0xc0052c7c50, 0x4}) /home/runner/work/templatefile/templatefile/templatefile.go:141 +0x391d github.com/TypedSoftware/templatefile.FuzzTemplatefile.func1(0xc005399880, {0xc004decd59, 0x6}, {0xc0052c7c50, 0x4}) /home/runner/work/templatefile/templatefile/templatefile_test.go:51 +0x146 reflect.Value.call({0x9fb9c0?, 0xa99598?, 0x13?}, {0xa6bf86, 0x4}, {0xc0053cf6e0, 0x3, 0x4?}) /opt/hostedtoolcache/go/1.25.9/x64/src/reflect/value.go:581 +0xcc6 reflect.Value.Call({0x9fb9c0?, 0xa99598?, 0xd8a540?}, {0xc0053cf6e0?, 0xa6b300?, 0xd21ae0?}) /opt/hostedtoolcache/go/1.25.9/x64/src/reflect/value.go:365 +0xb9 testing.(*F).Fuzz.func1.1(0xc005399880?) /opt/hostedtoolcache/go/1.25.9/x64/src/testing/fuzz.go:341 +0x32a testing.tRunner(0xc005399880, 0xc0053e0090) /opt/hostedtoolcache/go/1.25.9/x64/src/testing/testing.go:1934 +0xea created by testing.(*F).Fuzz.func1 in goroutine 21 /opt/hostedtoolcache/go/1.25.9/x64/src/testing/fuzz.go:328 +0x637 Failing input written to testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 To re-run: go test -run=FuzzTemplatefile/a3034cdd38718cd7 I've added the seed corpus as well. --- templatefile.go | 7 ++++++- testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 diff --git a/templatefile.go b/templatefile.go index a9ecb45..d44ee77 100644 --- a/templatefile.go +++ b/templatefile.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/hashicorp/hcl/v2/ext/tryfunc" "github.com/hashicorp/terraform/lang/funcs" ctyyaml "github.com/zclconf/go-cty-yaml" @@ -138,5 +140,8 @@ func templatefile(baseDir string, path string, vars string) (string, error) { return "", err } - return res.AsString(), err + if res.Type() != cty.String { + return "", fmt.Errorf("template result is %s, not string", res.Type().FriendlyName()) + } + return res.AsString(), nil } diff --git a/testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 b/testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 new file mode 100644 index 0000000..06d9461 --- /dev/null +++ b/testdata/fuzz/FuzzTemplatefile/a3034cdd38718cd7 @@ -0,0 +1,3 @@ +go test fuzz v1 +string("${val}") +string("val:")