Skip to content

Commit 2f8dd20

Browse files
committed
pkg/settings/cresettings: add/use WASM limits
1 parent c433c59 commit 2f8dd20

File tree

8 files changed

+162
-90
lines changed

8 files changed

+162
-90
lines changed

pkg/settings/cresettings/defaults.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
"WASMExecutionTimeout": "1m0s",
3232
"WASMMemoryLimit": "100mb",
3333
"WASMBinarySizeLimit": "30mb",
34+
"WASMCompressedBinarySizeLimit": "20mb",
35+
"WASMConfigSizeLimit": "30mb",
36+
"WASMSecretsSizeLimit": "30mb",
37+
"WASMResponseSizeLimit": "5mb",
3438
"ConsensusObservationSizeLimit": "10kb",
3539
"ConsensusCallsLimit": "2",
3640
"LogLineLimit": "1kb",
@@ -65,7 +69,7 @@
6569
"CallLimit": "2"
6670
},
6771
"HTTPAction": {
68-
"CallLimit": "3",
72+
"CallLimit": "5",
6973
"ResponseSizeLimit": "10kb",
7074
"ConnectionTimeout": "10s",
7175
"RequestSizeLimit": "100kb",

pkg/settings/cresettings/defaults.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ ExecutionResponseLimit = '100kb'
3131
WASMExecutionTimeout = '1m0s'
3232
WASMMemoryLimit = '100mb'
3333
WASMBinarySizeLimit = '30mb'
34+
WASMCompressedBinarySizeLimit = '20mb'
35+
WASMConfigSizeLimit = '30mb'
36+
WASMSecretsSizeLimit = '30mb'
37+
WASMResponseSizeLimit = '5mb'
3438
ConsensusObservationSizeLimit = '10kb'
3539
ConsensusCallsLimit = '2'
3640
LogLineLimit = '1kb'
@@ -66,7 +70,7 @@ ObservationSizeLimit = '10kb'
6670
CallLimit = '2'
6771

6872
[PerWorkflow.HTTPAction]
69-
CallLimit = '3'
73+
CallLimit = '5'
7074
ResponseSizeLimit = '10kb'
7175
ConnectionTimeout = '10s'
7276
RequestSizeLimit = '100kb'

pkg/settings/cresettings/settings.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ var Default = Schema{
7777
WASMExecutionTimeout: Duration(60 * time.Second),
7878
WASMMemoryLimit: Size(100 * config.MByte),
7979
WASMBinarySizeLimit: Size(30 * config.MByte),
80+
WASMCompressedBinarySizeLimit: Size(20 * config.MByte),
81+
WASMConfigSizeLimit: Size(30 * config.MByte),
82+
WASMSecretsSizeLimit: Size(30 * config.MByte),
83+
WASMResponseSizeLimit: Size(5 * config.MByte),
8084
ConsensusObservationSizeLimit: Size(10 * config.KByte),
8185
ConsensusCallsLimit: Int(2),
8286
LogLineLimit: Size(config.KByte),
@@ -113,7 +117,7 @@ var Default = Schema{
113117
CallLimit: Int(2),
114118
},
115119
HTTPAction: httpAction{
116-
CallLimit: Int(3),
120+
CallLimit: Int(5),
117121
ResponseSizeLimit: Size(10 * config.KByte),
118122
ConnectionTimeout: Duration(10 * time.Second),
119123
RequestSizeLimit: Size(100 * config.KByte),
@@ -163,9 +167,13 @@ type Workflows struct {
163167
ExecutionTimeout Setting[time.Duration]
164168
ExecutionResponseLimit Setting[config.Size]
165169

166-
WASMExecutionTimeout Setting[time.Duration]
167-
WASMMemoryLimit Setting[config.Size]
168-
WASMBinarySizeLimit Setting[config.Size]
170+
WASMExecutionTimeout Setting[time.Duration]
171+
WASMMemoryLimit Setting[config.Size]
172+
WASMBinarySizeLimit Setting[config.Size]
173+
WASMCompressedBinarySizeLimit Setting[config.Size]
174+
WASMConfigSizeLimit Setting[config.Size]
175+
WASMSecretsSizeLimit Setting[config.Size]
176+
WASMResponseSizeLimit Setting[config.Size]
169177

170178
// Deprecated: use Consensus.ObservationSizeLimit
171179
ConsensusObservationSizeLimit Setting[config.Size]

pkg/workflows/wasm/host/module.go

Lines changed: 100 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import (
2121
"github.com/bytecodealliance/wasmtime-go/v28"
2222
"google.golang.org/protobuf/proto"
2323

24+
"github.com/smartcontractkit/chainlink-common/pkg/config"
2425
"github.com/smartcontractkit/chainlink-common/pkg/custmsg"
2526
"github.com/smartcontractkit/chainlink-common/pkg/logger"
27+
"github.com/smartcontractkit/chainlink-common/pkg/settings"
28+
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
2629
dagsdk "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk"
2730
"github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm"
2831
wasmdagpb "github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/pb"
@@ -52,18 +55,22 @@ type DeterminismConfig struct {
5255
Seed int64
5356
}
5457
type ModuleConfig struct {
55-
TickInterval time.Duration
56-
Timeout *time.Duration
57-
MaxMemoryMBs uint64
58-
MinMemoryMBs uint64
59-
InitialFuel uint64
60-
Logger logger.Logger
61-
IsUncompressed bool
62-
Fetch func(ctx context.Context, req *FetchRequest) (*FetchResponse, error)
63-
MaxFetchRequests int
64-
MaxCompressedBinarySize uint64
65-
MaxDecompressedBinarySize uint64
66-
MaxResponseSizeBytes uint64
58+
TickInterval time.Duration
59+
Timeout *time.Duration
60+
MaxMemoryMBs uint64
61+
MinMemoryMBs uint64
62+
MemoryLimiter limits.BoundLimiter[config.Size] // supersedes Max/MinMemoryMBs if set
63+
InitialFuel uint64
64+
Logger logger.Logger
65+
IsUncompressed bool
66+
Fetch func(ctx context.Context, req *FetchRequest) (*FetchResponse, error)
67+
MaxFetchRequests int
68+
MaxCompressedBinarySize uint64
69+
MaxCompressedBinaryLimiter limits.BoundLimiter[config.Size] // supersedes MaxCompressedBinarySize if set
70+
MaxDecompressedBinarySize uint64
71+
MaxDecompressedBinaryLimiter limits.BoundLimiter[config.Size] // supersedes MaxDecompressedBinarySize if set
72+
MaxResponseSizeBytes uint64
73+
MaxResponseSizeLimiter limits.BoundLimiter[config.Size] // supersedes MaxResponseSizeBytes if set
6774

6875
MaxLogLenBytes uint32
6976
MaxLogCountDONMode uint32
@@ -143,7 +150,7 @@ func WithDeterminism() func(*ModuleConfig) {
143150
}
144151
}
145152

146-
func NewModule(modCfg *ModuleConfig, binary []byte, opts ...func(*ModuleConfig)) (*module, error) {
153+
func NewModule(ctx context.Context, modCfg *ModuleConfig, binary []byte, opts ...func(*ModuleConfig)) (*module, error) {
147154
// Apply options to the module config.
148155
for _, opt := range opts {
149156
opt(modCfg)
@@ -200,33 +207,59 @@ func NewModule(modCfg *ModuleConfig, binary []byte, opts ...func(*ModuleConfig))
200207
modCfg.MaxLogCountNodeMode = uint32(defaultMaxLogCountNodeMode)
201208
}
202209

203-
// Take the max of the min and the configured max memory mbs.
204-
// We do this because Go requires a minimum of 16 megabytes to run,
205-
// and local testing has shown that with less than the min, some
206-
// binaries may error sporadically.
207-
modCfg.MaxMemoryMBs = uint64(math.Max(float64(modCfg.MinMemoryMBs), float64(modCfg.MaxMemoryMBs)))
208-
209-
cfg := wasmtime.NewConfig()
210-
cfg.SetEpochInterruption(true)
211-
if modCfg.InitialFuel > 0 {
212-
cfg.SetConsumeFuel(true)
210+
lf := limits.Factory{Logger: modCfg.Logger}
211+
if modCfg.MemoryLimiter == nil {
212+
// Take the max of the min and the configured max memory mbs.
213+
// We do this because Go requires a minimum of 16 megabytes to run,
214+
// and local testing has shown that with less than the min, some
215+
// binaries may error sporadically.
216+
modCfg.MaxMemoryMBs = uint64(math.Max(float64(modCfg.MinMemoryMBs), float64(modCfg.MaxMemoryMBs)))
217+
limit := settings.Size(config.Size(modCfg.MaxMemoryMBs) * config.MByte)
218+
var err error
219+
modCfg.MemoryLimiter, err = limits.MakeBoundLimiter(lf, limit)
220+
if err != nil {
221+
return nil, fmt.Errorf("failed to make memory limiter: %w", err)
222+
}
223+
}
224+
if modCfg.MaxCompressedBinaryLimiter == nil {
225+
limit := settings.Size(config.Size(modCfg.MaxCompressedBinarySize))
226+
var err error
227+
modCfg.MaxCompressedBinaryLimiter, err = limits.MakeBoundLimiter(lf, limit)
228+
if err != nil {
229+
return nil, fmt.Errorf("failed to make compressed binary size limiter: %w", err)
230+
}
231+
}
232+
if modCfg.MaxDecompressedBinaryLimiter == nil {
233+
limit := settings.Size(config.Size(modCfg.MaxDecompressedBinarySize))
234+
var err error
235+
modCfg.MaxDecompressedBinaryLimiter, err = limits.MakeBoundLimiter(lf, limit)
236+
if err != nil {
237+
return nil, fmt.Errorf("failed to make decompressed binary size limiter: %w", err)
238+
}
239+
}
240+
if modCfg.MaxResponseSizeLimiter == nil {
241+
limit := settings.Size(config.Size(modCfg.MaxResponseSizeBytes))
242+
var err error
243+
modCfg.MaxResponseSizeLimiter, err = limits.MakeBoundLimiter(lf, limit)
244+
if err != nil {
245+
return nil, fmt.Errorf("failed to make response size limiter: %w", err)
246+
}
213247
}
214248

215-
cfg.CacheConfigLoadDefault()
216-
cfg.SetCraneliftOptLevel(wasmtime.OptLevelSpeedAndSize)
217-
218-
// Handled differenty based on host OS.
219-
SetUnwinding(cfg)
220-
221-
engine := wasmtime.NewEngineWithConfig(cfg)
222249
if !modCfg.IsUncompressed {
223250
// validate the binary size before decompressing
224251
// this is to prevent decompression bombs
225-
if uint64(len(binary)) > modCfg.MaxCompressedBinarySize {
226-
return nil, fmt.Errorf("compressed binary size exceeds the maximum allowed size of %d bytes", modCfg.MaxCompressedBinarySize)
252+
if err := modCfg.MaxCompressedBinaryLimiter.Check(ctx, config.SizeOf(binary)); err != nil {
253+
if errors.Is(err, limits.ErrorBoundLimited[config.Size]{}) {
254+
return nil, fmt.Errorf("compressed binary size exceeds the maximum allowed size of %d bytes: %w", modCfg.MaxCompressedBinarySize, err)
255+
}
256+
return nil, fmt.Errorf("failed to check compressed binary size limit: %w", err)
227257
}
228-
229-
rdr := io.LimitReader(brotli.NewReader(bytes.NewBuffer(binary)), int64(modCfg.MaxDecompressedBinarySize+1))
258+
maxDecompressedBinarySize, err := modCfg.MaxDecompressedBinaryLimiter.Limit(ctx)
259+
if err != nil {
260+
return nil, fmt.Errorf("failed to get decompressed binary size limit: %w", err)
261+
}
262+
rdr := io.LimitReader(brotli.NewReader(bytes.NewBuffer(binary)), int64(maxDecompressedBinarySize+1))
230263
decompedBinary, err := io.ReadAll(rdr)
231264
if err != nil {
232265
return nil, fmt.Errorf("failed to decompress binary: %w", err)
@@ -238,9 +271,27 @@ func NewModule(modCfg *ModuleConfig, binary []byte, opts ...func(*ModuleConfig))
238271
// Validate the decompressed binary size.
239272
// io.LimitReader prevents decompression bombs by reading up to a set limit, but it will not return an error if the limit is reached.
240273
// The Read() method will return io.EOF, and ReadAll will gracefully handle it and return nil.
241-
if uint64(len(binary)) > modCfg.MaxDecompressedBinarySize {
242-
return nil, fmt.Errorf("decompressed binary size reached the maximum allowed size of %d bytes", modCfg.MaxDecompressedBinarySize)
274+
if err := modCfg.MaxDecompressedBinaryLimiter.Check(ctx, config.SizeOf(binary)); err != nil {
275+
if errors.Is(err, limits.ErrorBoundLimited[config.Size]{}) {
276+
return nil, fmt.Errorf("decompressed binary size reached the maximum allowed size of %d bytes: %w", modCfg.MaxDecompressedBinarySize, err)
277+
}
278+
return nil, fmt.Errorf("failed to check decompressed binary size limit: %w", err)
279+
}
280+
281+
return newModule(modCfg, binary)
282+
}
283+
284+
func newModule(modCfg *ModuleConfig, binary []byte) (*module, error) {
285+
cfg := wasmtime.NewConfig()
286+
cfg.SetEpochInterruption(true)
287+
if modCfg.InitialFuel > 0 {
288+
cfg.SetConsumeFuel(true)
243289
}
290+
cfg.CacheConfigLoadDefault()
291+
cfg.SetCraneliftOptLevel(wasmtime.OptLevelSpeedAndSize)
292+
SetUnwinding(cfg) // Handled differenty based on host OS.
293+
294+
engine := wasmtime.NewEngineWithConfig(cfg)
244295

245296
mod, err := wasmtime.NewModule(engine, binary)
246297
if err != nil {
@@ -256,16 +307,14 @@ func NewModule(modCfg *ModuleConfig, binary []byte, opts ...func(*ModuleConfig))
256307
}
257308
}
258309

259-
m := &module{
310+
return &module{
260311
engine: engine,
261312
module: mod,
262313
wconfig: cfg,
263314
cfg: modCfg,
264315
stopCh: make(chan struct{}),
265316
v2ImportName: v2ImportName,
266-
}
267-
268-
return m, nil
317+
}, nil
269318
}
270319

271320
func linkNoDAG(m *module, store *wasmtime.Store, exec *execution[*sdkpb.ExecutionResult]) (*wasmtime.Instance, error) {
@@ -468,7 +517,7 @@ func (m *module) Run(ctx context.Context, request *wasmdagpb.Request) (*wasmdagp
468517
computeRequest := r.GetComputeRequest()
469518
if computeRequest != nil {
470519
computeRequest.RuntimeConfig = &wasmdagpb.RuntimeConfig{
471-
MaxResponseSizeBytes: int64(m.cfg.MaxResponseSizeBytes),
520+
MaxResponseSizeBytes: int64(maxSize),
472521
}
473522
}
474523
}
@@ -493,7 +542,11 @@ func runWasm[I, O proto.Message](
493542

494543
defer store.Close()
495544

496-
setMaxResponseSize(request, m.cfg.MaxResponseSizeBytes)
545+
maxResponseSizeBytes, err := m.cfg.MaxResponseSizeLimiter.Limit(ctx)
546+
if err != nil {
547+
return o, fmt.Errorf("failed to get response size limit: %w", err)
548+
}
549+
setMaxResponseSize(request, uint64(maxResponseSizeBytes))
497550
reqpb, err := proto.Marshal(request)
498551
if err != nil {
499552
return o, err
@@ -517,8 +570,12 @@ func runWasm[I, O proto.Message](
517570
}
518571

519572
// Limit memory to max memory megabytes per instance.
573+
maxMemoryBytes, err := m.cfg.MemoryLimiter.Limit(ctx)
574+
if err != nil {
575+
return o, fmt.Errorf("failed to get memory limit: %w", err)
576+
}
520577
store.Limiter(
521-
int64(m.cfg.MaxMemoryMBs)*int64(math.Pow(10, 6)),
578+
int64(maxMemoryBytes/config.MByte)*int64(math.Pow(10, 6)),
522579
-1, // tableElements, -1 == default
523580
1, // instances
524581
1, // tables

pkg/workflows/wasm/host/standard_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ func makeTestModuleByName(t *testing.T, testName string, cfg *ModuleConfig) *mod
519519
if cfg == nil {
520520
cfg = defaultNoDAGModCfg(t)
521521
}
522-
mod, err := NewModule(cfg, binary)
522+
mod, err := NewModule(t.Context(), cfg, binary)
523523
require.NoError(t, err)
524524
return mod
525525
}

pkg/workflows/wasm/host/wasm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
func GetWorkflowSpec(ctx context.Context, modCfg *ModuleConfig, binary []byte, config []byte) (*legacySdk.WorkflowSpec, error) {
16-
m, err := NewModule(modCfg, binary, WithDeterminism())
16+
m, err := NewModule(ctx, modCfg, binary, WithDeterminism())
1717
if err != nil {
1818
return nil, fmt.Errorf("could not instantiate module: %w", err)
1919
}

pkg/workflows/wasm/host/wasm_nodag_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
"github.com/smartcontractkit/chainlink-common/pkg/logger"
1414
"github.com/smartcontractkit/chainlink-protos/cre/go/sdk"
1515

16-
mock "github.com/stretchr/testify/mock"
16+
"github.com/stretchr/testify/mock"
1717
"github.com/stretchr/testify/require"
1818
)
1919

@@ -32,7 +32,7 @@ func Test_Sleep_Timeout(t *testing.T) {
3232
mc := defaultNoDAGModCfg(t)
3333
timeout := 1 * time.Second
3434
mc.Timeout = &timeout
35-
m, err := NewModule(mc, binary)
35+
m, err := NewModule(t.Context(), mc, binary)
3636
require.NoError(t, err)
3737

3838
m.v2ImportName = "test"
@@ -63,7 +63,7 @@ func Test_NoDag_Run(t *testing.T) {
6363

6464
t.Run("NOK fails with unset ExecutionHelper for trigger", func(t *testing.T) {
6565
mc := defaultNoDAGModCfg(t)
66-
m, err := NewModule(mc, binary)
66+
m, err := NewModule(t.Context(), mc, binary)
6767
require.NoError(t, err)
6868

6969
m.Start()
@@ -81,7 +81,7 @@ func Test_NoDag_Run(t *testing.T) {
8181

8282
t.Run("OK can subscribe without setting ExecutionHelper", func(t *testing.T) {
8383
mc := defaultNoDAGModCfg(t)
84-
m, err := NewModule(mc, binary)
84+
m, err := NewModule(t.Context(), mc, binary)
8585
require.NoError(t, err)
8686

8787
m.Start()
@@ -122,7 +122,7 @@ func Test_NoDAG_LoggingWithLimits(t *testing.T) {
122122

123123
binary := createTestBinary(loggingLimitsBinaryCmd, loggingLimitsBinaryLocation, true, t)
124124

125-
m, err := NewModule(cfg, binary)
125+
m, err := NewModule(t.Context(), cfg, binary)
126126
require.NoError(t, err)
127127

128128
_, err = m.Execute(t.Context(), executeRequest, mockExecutionHelper)

0 commit comments

Comments
 (0)