Skip to content

Commit edd0130

Browse files
Bound serialization resource consumption
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent f973ab4 commit edd0130

6 files changed

Lines changed: 310 additions & 21 deletions

File tree

src/functions/private/ConvertTo-YamlNode.ps1

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ function ConvertTo-YamlNode {
5151
))
5252
}
5353

54+
$isReference = Test-YamlSerializationReference -Value $frame.Value
55+
if ($isReference) {
56+
$firstTime = $false
57+
$frame.ReferenceId = $State.IdGenerator.GetId($frame.Value, [ref] $firstTime)
58+
if (-not $firstTime) {
59+
$State.ReferenceCounts[$frame.ReferenceId]++
60+
if ($State.Active.Contains($frame.ReferenceId)) {
61+
throw (New-YamlSerializationException -ErrorId 'YamlCycleDetected' -Message (
62+
"A cycle was detected while serializing type '$($frame.Value.GetType().FullName)'."
63+
))
64+
}
65+
$frame.Holder.Value = $State.NodesById[$frame.ReferenceId]
66+
[void] $stack.Pop()
67+
continue
68+
}
69+
$State.ReferenceCounts[$frame.ReferenceId] = 1
70+
$State.ReferenceOrder.Add($frame.ReferenceId)
71+
}
72+
5473
$frame.Shape = Get-YamlSerializationShape -Value $frame.Value -State $State `
5574
-EnumsAsStrings:$EnumsAsStrings
5675
if ($frame.Shape.Kind -eq 'Scalar') {
@@ -59,22 +78,6 @@ function ConvertTo-YamlNode {
5978
continue
6079
}
6180

62-
$firstTime = $false
63-
$frame.ReferenceId = $State.IdGenerator.GetId($frame.Value, [ref] $firstTime)
64-
if (-not $firstTime) {
65-
$State.ReferenceCounts[$frame.ReferenceId]++
66-
if ($State.Active.Contains($frame.ReferenceId)) {
67-
throw (New-YamlSerializationException -ErrorId 'YamlCycleDetected' -Message (
68-
"A cycle was detected while serializing type '$($frame.Value.GetType().FullName)'."
69-
))
70-
}
71-
$frame.Holder.Value = $State.NodesById[$frame.ReferenceId]
72-
[void] $stack.Pop()
73-
continue
74-
}
75-
76-
$State.ReferenceCounts[$frame.ReferenceId] = 1
77-
$State.ReferenceOrder.Add($frame.ReferenceId)
7881
if ($frame.Shape.Kind -eq 'Binary') {
7982
$frame.Shape.Node.ReferenceId = $frame.ReferenceId
8083
$State.NodesById[$frame.ReferenceId] = $frame.Shape.Node

src/functions/private/Get-YamlSerializationShape.ps1

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ function Get-YamlSerializationShape {
1313
[Parameter(Mandatory)]
1414
[pscustomobject] $State,
1515

16-
[switch] $EnumsAsStrings
16+
[switch] $EnumsAsStrings,
17+
18+
[switch] $InspectOnly
1719
)
1820

1921
$scalar = New-YamlEmissionNode -Kind Scalar
@@ -189,6 +191,15 @@ function Get-YamlSerializationShape {
189191
}
190192

191193
if ($isByteArray) {
194+
$base64Length = [long] 4 * [long] [System.Math]::Ceiling($Value.LongLength / 3.0)
195+
if ($base64Length -gt $State.MaxScalarLength) {
196+
throw (New-YamlSerializationException -ErrorId 'YamlScalarLimitExceeded' -Message (
197+
"A scalar exceeds the configured limit of $($State.MaxScalarLength) characters."
198+
))
199+
}
200+
if ($InspectOnly) {
201+
return [pscustomobject]@{ Kind = 'Binary'; Node = $null; Values = $null }
202+
}
192203
$scalar.Tag = 'tag:yaml.org,2002:binary'
193204
$scalar.Value = [System.Convert]::ToBase64String($Value)
194205
$scalar.Style = 'DoubleQuoted'
@@ -197,16 +208,33 @@ function Get-YamlSerializationShape {
197208
}
198209

199210
if ($isDictionary -or $isPropertyBag) {
211+
if ($InspectOnly) {
212+
return [pscustomobject]@{ Kind = 'Mapping'; Node = $null; Values = $null }
213+
}
200214
$entries = [System.Collections.Generic.List[object]]::new()
201215
if ($isDictionary) {
202-
foreach ($entry in $Value.GetEnumerator()) {
216+
$remainingNodes = [System.Math]::Max(0, $State.MaxNodes - $State.NodeCount)
217+
$maximumEntries = [int] [System.Math]::Floor($remainingNodes / 2.0)
218+
$rawEntries = Read-YamlBoundedEnumerable -Value $Value `
219+
-MaximumItems $maximumEntries -MaxNodes $State.MaxNodes -State $State `
220+
-DictionaryEntries -EnumsAsStrings:$EnumsAsStrings
221+
foreach ($entry in $rawEntries) {
203222
$entries.Add([pscustomobject]@{
204223
Key = [object] $entry.Key
205224
Value = [object] $entry.Value
206225
})
207226
}
208227
} else {
228+
if (($dataProperties.Count * 2) -gt ($State.MaxNodes - $State.NodeCount)) {
229+
throw (New-YamlSerializationException -ErrorId 'YamlNodeLimitExceeded' -Message (
230+
"The object graph exceeds the configured limit of $($State.MaxNodes) nodes."
231+
))
232+
}
209233
foreach ($property in $dataProperties) {
234+
$null = Get-YamlSerializationShape -Value $property.Name -State $State `
235+
-EnumsAsStrings:$EnumsAsStrings -InspectOnly
236+
$null = Get-YamlSerializationShape -Value $property.Value -State $State `
237+
-EnumsAsStrings:$EnumsAsStrings -InspectOnly
210238
$entries.Add([pscustomobject]@{
211239
Key = [object] $property.Name
212240
Value = [object] $property.Value
@@ -220,10 +248,13 @@ function Get-YamlSerializationShape {
220248
}
221249
}
222250
if ($isSequence) {
223-
$items = [System.Collections.Generic.List[object]]::new()
224-
foreach ($item in $Value) {
225-
$items.Add([object] $item)
251+
if ($InspectOnly) {
252+
return [pscustomobject]@{ Kind = 'Sequence'; Node = $null; Values = $null }
226253
}
254+
$remainingNodes = [System.Math]::Max(0, $State.MaxNodes - $State.NodeCount)
255+
$items = Read-YamlBoundedEnumerable -Value $Value `
256+
-MaximumItems $remainingNodes -MaxNodes $State.MaxNodes -State $State `
257+
-EnumsAsStrings:$EnumsAsStrings
227258
return [pscustomobject]@{
228259
Kind = 'Sequence'
229260
Node = $null
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
function Read-YamlBoundedEnumerable {
2+
<#
3+
.SYNOPSIS
4+
Materializes only as many enumerable items as the node budget permits.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([System.Collections.Generic.List[object]])]
8+
param (
9+
[Parameter(Mandatory)]
10+
[System.Collections.IEnumerable] $Value,
11+
12+
[Parameter(Mandatory)]
13+
[ValidateRange(0, 2147483647)]
14+
[int] $MaximumItems,
15+
16+
[Parameter(Mandatory)]
17+
[int] $MaxNodes,
18+
19+
[Parameter(Mandatory)]
20+
[pscustomobject] $State,
21+
22+
[switch] $DictionaryEntries,
23+
24+
[switch] $EnumsAsStrings
25+
)
26+
27+
if ($Value -is [System.Collections.ICollection] -and
28+
$Value.Count -gt $MaximumItems) {
29+
throw (New-YamlSerializationException -ErrorId 'YamlNodeLimitExceeded' -Message (
30+
"The object graph exceeds the configured limit of $MaxNodes nodes."
31+
))
32+
}
33+
34+
$items = [System.Collections.Generic.List[object]]::new()
35+
$enumerator = $Value.GetEnumerator()
36+
try {
37+
while ($enumerator.MoveNext()) {
38+
if ($items.Count -ge $MaximumItems) {
39+
throw (New-YamlSerializationException -ErrorId 'YamlNodeLimitExceeded' -Message (
40+
"The object graph exceeds the configured limit of $MaxNodes nodes."
41+
))
42+
}
43+
$item = [object] $enumerator.Current
44+
if ($DictionaryEntries) {
45+
$null = Get-YamlSerializationShape -Value $item.Key -State $State `
46+
-EnumsAsStrings:$EnumsAsStrings -InspectOnly
47+
$null = Get-YamlSerializationShape -Value $item.Value -State $State `
48+
-EnumsAsStrings:$EnumsAsStrings -InspectOnly
49+
} else {
50+
$null = Get-YamlSerializationShape -Value $item -State $State `
51+
-EnumsAsStrings:$EnumsAsStrings -InspectOnly
52+
}
53+
$items.Add($item)
54+
}
55+
} finally {
56+
if ($enumerator -is [System.IDisposable]) {
57+
$enumerator.Dispose()
58+
}
59+
}
60+
Write-Output -InputObject $items -NoEnumerate
61+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function Test-YamlSerializationReference {
2+
<#
3+
.SYNOPSIS
4+
Tests whether a value participates in YAML anchor identity.
5+
#>
6+
[CmdletBinding()]
7+
[OutputType([bool])]
8+
param (
9+
[Parameter()]
10+
[AllowNull()]
11+
[object] $Value
12+
)
13+
14+
if ($null -eq $Value -or $Value -is [System.DBNull]) {
15+
return $false
16+
}
17+
if ($Value -is [byte[]] -or
18+
$Value -is [System.Collections.IDictionary] -or
19+
$Value -is [System.Management.Automation.PSCustomObject]) {
20+
return $true
21+
}
22+
if ($Value -is [System.Collections.IEnumerable] -and
23+
$Value -isnot [string] -and
24+
$Value -isnot [char]) {
25+
return $true
26+
}
27+
return $Value.GetType() -eq [object]
28+
}

src/functions/public/ConvertTo-Yaml.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,31 @@ function ConvertTo-Yaml {
7676

7777
begin {
7878
$values = [System.Collections.Generic.List[object]]::new()
79+
$inspectionState = [pscustomobject]@{
80+
MaxScalarLength = $MaxScalarLength
81+
MaxNodes = $MaxNodes
82+
NodeCount = 0
83+
}
7984
}
8085
process {
86+
try {
87+
$null = Get-YamlSerializationShape -Value $InputObject -State $inspectionState `
88+
-EnumsAsStrings:$EnumsAsStrings -InspectOnly
89+
if ($values.Count -gt 0 -and ($values.Count + 2) -gt $MaxNodes) {
90+
throw (New-YamlSerializationException -ErrorId 'YamlNodeLimitExceeded' -Message (
91+
"The object graph exceeds the configured limit of $MaxNodes nodes."
92+
))
93+
}
94+
} catch [System.NotSupportedException] {
95+
$record = New-YamlErrorRecord -Exception $_.Exception `
96+
-DefaultErrorId 'YamlUnsupportedType' -Category InvalidType -TargetObject $InputObject
97+
$PSCmdlet.ThrowTerminatingError($record)
98+
} catch [System.InvalidOperationException] {
99+
$record = New-YamlErrorRecord -Exception $_.Exception `
100+
-DefaultErrorId 'YamlSerializationFailed' -Category InvalidOperation `
101+
-TargetObject $InputObject
102+
$PSCmdlet.ThrowTerminatingError($record)
103+
}
81104
$values.Add($InputObject)
82105
}
83106
end {

0 commit comments

Comments
 (0)