fix(runtime): parallel-safe decompression limit; tighten ParsedDeploymentArtifact surface
This commit is contained in:
+26
-23
@@ -410,33 +410,36 @@ public sealed class ParsedDeploymentArtifactTests
|
||||
// A hostile/corrupt blob must not be able to OOM the node: gzip of a highly repetitive payload
|
||||
// reaches ~1000:1, so a few-hundred-KB artifact row could inflate to hundreds of MB. The production
|
||||
// limit is 512 MB, which no honest artifact approaches — crafting a real 512 MB expansion in a unit
|
||||
// test would burn seconds and gigabytes, so the limit is an internal seam this test lowers instead.
|
||||
var original = DeploymentArtifact.MaxDecompressedBytes;
|
||||
try
|
||||
{
|
||||
var payload = new byte[64 * 1024]; // all zeroes: compresses to a few dozen bytes
|
||||
var bomb = Gzip(payload);
|
||||
bomb.Length.ShouldBeLessThan(1024); // the small-in / huge-out shape the guard exists for
|
||||
// test would burn seconds and gigabytes, so the ceiling is a parameter on the internal Inflate seam
|
||||
// and this test passes a small one.
|
||||
//
|
||||
// It used to lower a mutable DeploymentArtifact.MaxDecompressedBytes static and restore it in a
|
||||
// finally. xUnit runs test CLASSES in parallel, so that window was visible to every sibling class
|
||||
// parsing a >4 KB artifact concurrently — they would fail with "refusing to inflate" for reasons
|
||||
// having nothing to do with them. Do not reintroduce a process-wide mutation here.
|
||||
var payload = new byte[64 * 1024]; // all zeroes: compresses to a few dozen bytes
|
||||
var bomb = Gzip(payload);
|
||||
bomb.Length.ShouldBeLessThan(1024); // the small-in / huge-out shape the guard exists for
|
||||
|
||||
DeploymentArtifact.MaxDecompressedBytes = 4096;
|
||||
var ex = Should.Throw<JsonException>(() => DeploymentArtifact.Inflate(bomb, maxBytes: 4096));
|
||||
ex.Message.ShouldContain("4096");
|
||||
ex.Message.ShouldContain("refusing to inflate");
|
||||
|
||||
var ex = Should.Throw<JsonException>(() => DeploymentArtifact.Open(bomb));
|
||||
ex.Message.ShouldContain("4096");
|
||||
// Falsifiability control: the SAME bytes at the production ceiling inflate to the full payload — so
|
||||
// the throw above is the guard firing, not the gzip stream being unreadable.
|
||||
DeploymentArtifact.Inflate(bomb, DeploymentArtifact.MaxDecompressedBytes).Length.ShouldBe(payload.Length);
|
||||
|
||||
// And the lenient statics swallow it exactly like malformed JSON.
|
||||
DeploymentArtifact.ParseComposition(bomb).EquipmentNodes.ShouldBeEmpty();
|
||||
DeploymentArtifact.ParseDriverInstances(bomb).ShouldBeEmpty();
|
||||
// …and the guard is actually wired into the door every consumer comes through: Open() at the
|
||||
// production ceiling gets past decompression and fails as JSON (64 KB of NULs is not JSON), never
|
||||
// as a bomb.
|
||||
Should.Throw<JsonException>(() => DeploymentArtifact.Open(bomb))
|
||||
.Message.ShouldNotContain("refusing to inflate");
|
||||
|
||||
// Falsifiability control: under the production limit these same bytes decompress fine (and then
|
||||
// fail as JSON, not as a bomb) — so the assertion above is the guard firing, not gzip failing.
|
||||
DeploymentArtifact.MaxDecompressedBytes = original;
|
||||
Should.Throw<JsonException>(() => DeploymentArtifact.Open(bomb))
|
||||
.Message.ShouldNotContain("refusing to inflate");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeploymentArtifact.MaxDecompressedBytes = original;
|
||||
}
|
||||
// The over-limit JsonException lands in the same lenient catch the static wrappers already own —
|
||||
// that leniency is proved on the corrupt-gzip path in Static_wrappers_stay_lenient_on_corrupt_gzip,
|
||||
// which reaches it through the identical `catch (JsonException)` in every wrapper. Asserting it here
|
||||
// with these bytes would be vacuous: 64 KB of NULs comes back empty from a lenient wrapper whether
|
||||
// the guard fired or the JSON parse did.
|
||||
}
|
||||
|
||||
private static byte[] Gzip(byte[] plain)
|
||||
|
||||
Reference in New Issue
Block a user