fix(serverless): enable system clocks for wasm modules

- opt into `WithSysWalltime` and `WithSysNanotime` to prevent wazero from using a frozen sentinel clock
- add regression tests to verify real-time clock behavior in wasm execution
- ensure serverless functions receive accurate timestamps for audit and cursor logic
This commit is contained in:
anonpenguin23 2026-05-26 10:53:07 +03:00
parent 1399b22676
commit 8fbc4485c1
3 changed files with 219 additions and 2 deletions

View File

@ -458,7 +458,16 @@ func (e *Engine) InstantiatePersistent(ctx context.Context, fn *Function, invCtx
WithStdin(emptyReader{}).
WithStdout(discardWriter{}).
WithStderr(discardWriter{}).
WithArgs(fn.Name)
WithArgs(fn.Name).
// Bugboard #27 — wazero defaults to fake/sentinel clocks (deterministic
// fixtures for unit testing). TinyGo wasm calls WASI clock_time_get
// from time.Now() and gets a frozen ~2022-01-01T00:00:00.001Z back
// for every reading, silently poisoning any serverless function that
// embeds timestamps (receipts, audit rows, cursor cmp logic). Opt
// into real clocks via the documented wazero hook — same effect as
// the runtime would get on a normal Go process.
WithSysWalltime().
WithSysNanotime()
instance, err := e.runtime.InstantiateModule(ctx, compiled, moduleConfig)
if err != nil {

View File

@ -73,7 +73,14 @@ func (e *Executor) ExecuteModule(ctx context.Context, compiled wazero.CompiledMo
WithStdin(stdin).
WithStdout(stdout).
WithStderr(stderr).
WithArgs(moduleName) // argv[0] is the program name
WithArgs(moduleName). // argv[0] is the program name
// Bugboard #27 — wazero defaults to fake/sentinel clocks. Without
// these opt-ins, TinyGo's time.Now() returns ~2022-01-01T00:00:00.001Z
// frozen on every read, silently poisoning timestamps in every
// invocation that uses time.Now() (receipts, audit rows, cursor cmp).
// Same fix applied at engine.go for the persistent-WS path.
WithSysWalltime().
WithSysNanotime()
// Acquire concurrency slot
if e.sem != nil {

View File

@ -0,0 +1,201 @@
package execution
import (
"context"
"encoding/binary"
"testing"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
// Bugboard #27 — wazero defaults to a FAKE walltime clock that returns
// ~2022-01-01T00:00:00.001Z frozen on every reading. TinyGo wasm calls
// WASI clock_time_get from time.Now(), so any serverless function that
// embeds timestamps (receipts, audit rows, cursor comparisons) silently
// poisons its writes with the sentinel epoch.
//
// The fix is to opt into real clocks via .WithSysWalltime() /
// .WithSysNanotime() on the wazero ModuleConfig (one-line per the two
// moduleConfig builders — engine.go for persistent WS, executor.go for
// stateless). This test pins the behavior at the executor's config
// path: instantiate a tiny wasm that calls WASI clock_time_get, read
// the result, assert it's a real post-2024 epoch and not the frozen
// 2022 sentinel.
//
// If a future refactor drops .WithSysWalltime(), this test fails with
// "got pre-2024 timestamp X (sentinel?); did the WithSysWalltime() call
// get dropped from moduleConfig?" — exact line back to the regression.
// walltimeProbeWasm is a hand-assembled WASM module that imports
// wasi_snapshot_preview1.clock_time_get and calls it from _start,
// writing the result to memory[0:8].
//
// (module
// (type $clock_time_get (func (param i32 i64 i32) (result i32)))
// (type $start (func))
// (import "wasi_snapshot_preview1" "clock_time_get"
// (func $clock_time_get (type 0)))
// (memory (export "memory") 1)
// (func $_start (type 1)
// i32.const 0 ;; clock_id = REALTIME (0)
// i64.const 0 ;; precision = 0
// i32.const 0 ;; out_ptr = 0
// call $clock_time_get
// drop)
// (export "_start" (func $_start)))
//
// Reference: https://webassembly.github.io/spec/core/binary/modules.html
var walltimeProbeWasm = []byte{
// Magic + version
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
// Type section (id=1) — body=11 bytes
0x01,
0x0b, // size = 11
0x02, // 2 types
0x60, 0x03, 0x7f, 0x7e, 0x7f, // type 0: func(i32, i64, i32)
0x01, 0x7f, // -> (i32)
0x60, 0x00, 0x00, // type 1: func() -> ()
// Import section (id=2) — body=0x29 (41 bytes)
0x02,
0x29,
0x01, // 1 import
0x16, // module name "wasi_snapshot_preview1" length=22
0x77, 0x61, 0x73, 0x69, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x31,
0x0e, // fn name "clock_time_get" length=14
0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x67, 0x65, 0x74,
0x00, 0x00, // kind=func, type idx=0
// Function section (id=3) — body=2 bytes
0x03,
0x02,
0x01, // 1 function
0x01, // type idx 1 (for _start)
// Memory section (id=5) — body=3 bytes
0x05,
0x03,
0x01, // 1 memory
0x00, 0x01, // limits: flags=0 (no max), min=1 page
// Export section (id=7) — body=19 bytes (0x13)
// = count(1) + memory_export(9) + start_export(9) = 19
0x07,
0x13,
0x02, // 2 exports
0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, // "memory"
0x02, 0x00, // kind=memory, idx=0
0x06, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, // "_start"
0x00, 0x01, // kind=func, idx=1 (after the 1 import)
// Code section (id=10) — body=13 bytes (0x0d)
// = count(1) + body_size_byte(1) + body(11) = 13
0x0a,
0x0d,
0x01, // 1 function body
0x0b, // body size = 11 (locals_count + 10 instr bytes)
0x00, // 0 local groups
0x41, 0x00, // i32.const 0 (clock_id)
0x42, 0x00, // i64.const 0 (precision)
0x41, 0x00, // i32.const 0 (out_ptr)
0x10, 0x00, // call func 0 (the imported clock_time_get)
0x1a, // drop (errno return)
0x0b, // end
}
func TestModuleConfig_walltimeIsRealNotFrozenSentinel(t *testing.T) {
ctx := context.Background()
runtime := wazero.NewRuntime(ctx)
defer runtime.Close(ctx)
if _, err := wasi_snapshot_preview1.Instantiate(ctx, runtime); err != nil {
t.Fatalf("instantiate WASI: %v", err)
}
compiled, err := runtime.CompileModule(ctx, walltimeProbeWasm)
if err != nil {
t.Fatalf("compile probe wasm: %v (hex assembly likely off; recompute section sizes)", err)
}
defer compiled.Close(ctx)
// Mirror the executor.go moduleConfig — the assertion is that this
// SAME config is what protects against the bug-#27 regression.
moduleConfig := wazero.NewModuleConfig().
WithName("").
WithArgs("probe").
WithSysWalltime().
WithSysNanotime()
mod, err := runtime.InstantiateModule(ctx, compiled, moduleConfig)
if err != nil {
t.Fatalf("instantiate probe module: %v", err)
}
defer mod.Close(ctx)
mem := mod.Memory()
if mem == nil {
t.Fatal("probe module has no memory export")
}
raw, ok := mem.Read(0, 8)
if !ok {
t.Fatal("could not read 8 bytes from probe memory at offset 0")
}
gotNs := binary.LittleEndian.Uint64(raw)
// 2024-01-01T00:00:00 in unix nanoseconds = 1704067200000000000.
// Any real time after that passes. The sentinel ~2022-01-01 fails.
const cutoff2024Ns uint64 = 1704067200000000000
if gotNs < cutoff2024Ns {
gotTime := time.Unix(0, int64(gotNs))
t.Errorf("BUG #27 REGRESSION: WASI clock_time_get returned %d ns (%s) — "+
"pre-2024 means the fake/sentinel clock is in effect. "+
"Did the .WithSysWalltime() call get dropped from moduleConfig "+
"in executor.go or engine.go?", gotNs, gotTime)
}
}
func TestModuleConfig_walltimeWithoutFix_demoSentinel(t *testing.T) {
// Negative control: WITHOUT .WithSysWalltime(), confirm wazero
// returns the frozen sentinel. This pins the *cause* (so anyone
// reading the test understands why the positive test is meaningful).
// If wazero ever changes its default to a real clock, this test
// fails — at which point the bug is moot and both tests can be
// retired. Pinning the negative makes that change visible instead
// of silently invalidating the fix's necessity.
ctx := context.Background()
runtime := wazero.NewRuntime(ctx)
defer runtime.Close(ctx)
if _, err := wasi_snapshot_preview1.Instantiate(ctx, runtime); err != nil {
t.Fatalf("instantiate WASI: %v", err)
}
compiled, err := runtime.CompileModule(ctx, walltimeProbeWasm)
if err != nil {
t.Fatalf("compile probe wasm: %v", err)
}
defer compiled.Close(ctx)
// Default config — NO WithSysWalltime.
defaultConfig := wazero.NewModuleConfig().WithName("").WithArgs("probe")
mod, err := runtime.InstantiateModule(ctx, compiled, defaultConfig)
if err != nil {
t.Fatalf("instantiate probe module: %v", err)
}
defer mod.Close(ctx)
raw, _ := mod.Memory().Read(0, 8)
gotNs := binary.LittleEndian.Uint64(raw)
const cutoff2024Ns uint64 = 1704067200000000000
if gotNs >= cutoff2024Ns {
t.Logf("wazero default walltime is now %d ns (%s) — past 2024. "+
"If this happened upstream-by-default, the bug-#27 fix is no longer "+
"necessary and the .WithSysWalltime() opt-in can be retired.",
gotNs, time.Unix(0, int64(gotNs)))
t.Skip("wazero default walltime is real time — bug-#27 fix may be redundant; review")
}
// Sentinel confirmed → fix is meaningful.
}