Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
configure the limit. [#2923](https://github.com/d2lang/d2/pull/2923)
- Honor cancellation while expanding substitutions and materializing compiled
graphs. [#2923](https://github.com/d2lang/d2/pull/2923)
- Limit recursive glob matching and generated-field work so small diagrams
fail with a clear error instead of consuming excessive CPU or memory.
[#2925](https://github.com/d2lang/d2/pull/2925)
- decoding and assets:
- Cap decompressed URL-encoded D2 input at 16 MiB. [#2902](https://github.com/d2lang/d2/pull/2902)
- Bound image references, locators, fetched and decoded bytes, cached data, and
Expand Down
5 changes: 5 additions & 0 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type CompileOptions struct {
// MaxVariableExpansion bounds work added by variable substitutions and the
// automatic copies they induce. Zero uses d2ir.DefaultMaxVariableExpansion.
MaxVariableExpansion int64
// MaxGlobExpansion bounds work performed by glob matching and
// materialization. Zero uses d2ir.DefaultMaxGlobExpansion. Explicit source
// fields are not counted as materialization work.
MaxGlobExpansion int64
// FS is the file system used for resolving imports in the D2 text. Nil
// disables imports. Callers that accept untrusted input should prefer a
// filesystem constrained to the intended import root; lib/localfile provides
Expand All @@ -57,6 +61,7 @@ func Compile(p string, r io.Reader, opts *CompileOptions) (*d2graph.Graph, *d2ta
Context: opts.Context,
UTF16Pos: opts.UTF16Pos,
MaxVariableExpansion: opts.MaxVariableExpansion,
MaxGlobExpansion: opts.MaxGlobExpansion,
FS: opts.FS,
})
if err != nil {
Expand Down
17 changes: 17 additions & 0 deletions d2compiler/glob_expansion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package d2compiler_test

import (
"strings"
"testing"

"github.com/d2lang/d2/d2compiler"
)

func TestCompilePropagatesGlobExpansionLimit(t *testing.T) {
_, _, err := d2compiler.Compile("glob-feedback.d2", strings.NewReader("**.a\n**.b\n**.c\nx\n"), &d2compiler.CompileOptions{
MaxGlobExpansion: 64,
})
if err == nil || !strings.Contains(err.Error(), "glob expansion exceeds limit of 64 work units") {
t.Fatalf("Compile() error = %v, want glob expansion limit", err)
}
}
61 changes: 48 additions & 13 deletions d2ir/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ type compiler struct {
ctx context.Context
contextErr error
expansionErr error
globExpansionErr error
halted bool
variableExpansion *variableExpansionBudget
globExpansion *globExpansionBudget

fs fs.FS
imports []string
Expand Down Expand Up @@ -81,6 +83,10 @@ type CompileOptions struct {
// MaxVariableExpansion bounds work added by substitutions and automatic
// copies. Zero uses DefaultMaxVariableExpansion.
MaxVariableExpansion int64
// MaxGlobExpansion bounds work performed by glob matching and
// materialization. Zero uses DefaultMaxGlobExpansion. Explicit source fields
// are not counted as materialization work.
MaxGlobExpansion int64
// FS resolves imports. Nil disables imports. The lib/localfile package
// provides rooted and explicit unrestricted host-filesystem policies.
FS fs.FS
Expand All @@ -105,11 +111,16 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
if err != nil {
return nil, nil, err
}
globExpansion, err := newGlobExpansionBudget(opts.MaxGlobExpansion)
if err != nil {
return nil, nil, err
}
c := &compiler{
err: &d2parser.ParseError{},
ctx: ctx,
fs: opts.FS,
variableExpansion: variableExpansion,
globExpansion: globExpansion,

seenImports: make(map[string]struct{}),
parsedImports: make(map[string]*d2ast.Map),
Expand All @@ -132,25 +143,22 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
if c.contextErr != nil {
return nil, nil, c.contextErr
}
if err := c.compileLimitError(); err != nil {
return nil, nil, err
}
c.compileSubstitutions(m, nil)
if c.contextErr != nil {
return nil, nil, c.contextErr
}
if c.expansionErr != nil {
if !c.err.Empty() {
return nil, nil, c.err
}
return nil, nil, c.expansionErr
if err := c.compileLimitError(); err != nil {
return nil, nil, err
}
c.overlayClasses(m)
if c.contextErr != nil {
return nil, nil, c.contextErr
}
if c.expansionErr != nil {
if !c.err.Empty() {
return nil, nil, c.err
}
return nil, nil, c.expansionErr
if err := c.compileLimitError(); err != nil {
return nil, nil, err
}
// Substitutions can grow shared nodes after an earlier alias inserted them
// (for example through a forward scalar chain in an array spread). Recheck
Expand All @@ -162,15 +170,28 @@ func Compile(ast *d2ast.Map, opts *CompileOptions) (*Map, []string, error) {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
if c.expansionErr != nil && c.err.Empty() {
return nil, nil, c.expansionErr
if err := c.compileLimitError(); err != nil {
return nil, nil, err
}
if !c.err.Empty() {
return nil, nil, c.err
}
return m, c.imports, nil
}

func (c *compiler) compileLimitError() error {
if c.expansionErr == nil && c.globExpansionErr == nil {
return nil
}
if !c.err.Empty() {
return c.err
}
if c.expansionErr != nil {
return c.expansionErr
}
return c.globExpansionErr
}

func (c *compiler) overlayClasses(m *Map) {
if c.stopped() {
return
Expand Down Expand Up @@ -1001,6 +1022,11 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
})
case n.Substitution != nil:
// placeholder field to be resolved at the end
if len(c.globRefContextStack) > 0 {
if !c.reserveGlobGeneratedFieldWork(dst, n.Substitution) || !c.reserveGlobField(n.Substitution) {
return
}
}
f := &Field{
parent: dst,
Primary_: &Scalar{
Expand Down Expand Up @@ -1117,6 +1143,11 @@ func (c *compiler) compileKey(refctx *RefContext) {
return
}
postTargetStart := len(c.lazyPostTargets)
if refctx.Key.HasGlob() || len(c.globRefContextStack) > 0 {
if !c.reserveGlobWork(refctx.Key, 1) {
return
}
}
if refctx.Key.HasGlob() {
for _, refctx2 := range c.globRefContextStack {
if refctx.Equal(refctx2) {
Expand Down Expand Up @@ -2239,8 +2270,12 @@ func (c *compiler) compileArray(dst *Array, a *d2ast.Array, scopeAST *d2ast.Map)
if c.stopped() {
return
}
arrayNode := an.Unbox()
if len(c.globRefContextStack) > 0 && !c.reserveGlobWork(arrayNode, 1) {
return
}
var irv Value
switch v := an.Unbox().(type) {
switch v := arrayNode.(type) {
case *d2ast.Array:
ira := &Array{
parent: dst,
Expand Down
44 changes: 36 additions & 8 deletions d2ir/d2ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,12 @@ func (m *Map) ensureFieldMode(kp *d2ast.KeyPath, refctx *RefContext, create bool
}

func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create bool, gctx *globContext, c *compiler, indexed bool, fa, created *[]*Field) error {
visitGlobCandidate := func(*Field) bool {
if c == nil || gctx == nil {
return true
}
return c.reserveGlobWork(c.globSource(refctx), 1)
}
filter := func(f *Field, passthrough bool) bool {
if gctx != nil {
var ks string
Expand Down Expand Up @@ -1255,10 +1261,13 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
var multi bool
if c != nil && c.lazyGlobTarget != nil && gctx != nil &&
(d2ast.IsDoubleGlob(us.Pattern) || d2ast.IsTripleGlob(us.Pattern)) {
fa2 = m.multiGlobMatchesToward(c.lazyGlobTarget, us.Pattern)
fa2 = m.multiGlobMatchesToward(c.lazyGlobTarget, us.Pattern, visitGlobCandidate)
multi = true
} else {
fa2, multi = m.multiGlob(us.Pattern)
fa2, multi = m.multiGlob(us.Pattern, visitGlobCandidate)
}
if c != nil && c.stopped() {
return nil
}
if multi {
if i == len(kp.Path)-1 {
Expand Down Expand Up @@ -1290,6 +1299,9 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
}
}
for _, f := range fields {
if !visitGlobCandidate(f) {
return nil
}
if f.Name == nil {
continue
}
Expand Down Expand Up @@ -1346,12 +1358,14 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
if f := existing; f != nil {
// Don't add references for fake common KeyPath from trimCommon in CreateEdge.
if refctx != nil {
dueToGlob := c != nil && len(c.globRefContextStack) > 0
dueToLazyGlob := c != nil && c.lazyGlobBeingApplied
f.appendReference(&FieldReference{
String: kp.Path[i].Unbox(),
KeyPath: kp,
Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
DueToGlob_: dueToGlob,
DueToLazyGlob_: dueToLazyGlob,
})
}

Expand All @@ -1376,18 +1390,25 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
if !create {
return nil
}
if _, ok := d2ast.ReservedKeywords[strings.ToLower(head.ScalarString())]; !(ok && head.IsUnquoted()) && len(c.globRefContextStack) > 0 {
if _, ok := d2ast.ReservedKeywords[strings.ToLower(head.ScalarString())]; !(ok && head.IsUnquoted()) && c != nil && len(c.globRefContextStack) > 0 {
shape := ParentShape(m)
if shape == d2target.ShapeClass || shape == d2target.ShapeSQLTable {
return nil
}
}
var globSource d2ast.Node
if c != nil && len(c.globRefContextStack) > 0 {
globSource = c.globSource(refctx)
if !c.reserveGlobGeneratedFieldWork(m, globSource) {
return nil
}
}
f := &Field{
parent: m,
Name: kp.Path[i].Unbox(),
}
defer func() {
if i < kp.FirstGlob() {
if c == nil || i < kp.FirstGlob() {
return
}
for _, grefctx := range c.globRefContextStack {
Expand All @@ -1403,17 +1424,24 @@ func (m *Map) ensureField(i int, kp *d2ast.KeyPath, refctx *RefContext, create b
}()
// Don't add references for fake common KeyPath from trimCommon in CreateEdge.
if refctx != nil {
dueToGlob := c != nil && len(c.globRefContextStack) > 0
dueToLazyGlob := c != nil && c.lazyGlobBeingApplied
f.appendReference(&FieldReference{
String: kp.Path[i].Unbox(),
KeyPath: kp,
Context_: refctx,
DueToGlob_: len(c.globRefContextStack) > 0,
DueToLazyGlob_: c.lazyGlobBeingApplied,
DueToGlob_: dueToGlob,
DueToLazyGlob_: dueToLazyGlob,
})
}
if !filter(f, true) {
return nil
}
if c != nil && len(c.globRefContextStack) > 0 {
if !c.reserveGlobField(globSource) {
return nil
}
}
m.appendField(f)
*created = append(*created, f)
if i+1 == len(kp.Path) {
Expand Down
3 changes: 3 additions & 0 deletions d2ir/expansion.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func (c *compiler) stopped() bool {
if c.variableExpansion == nil {
c.variableExpansion = &variableExpansionBudget{limit: DefaultMaxVariableExpansion}
}
if c.globExpansion == nil {
c.globExpansion = &globExpansionBudget{limit: DefaultMaxGlobExpansion}
}
if err := c.ctx.Err(); err != nil {
c.contextErr = err
c.halted = true
Expand Down
Loading
Loading