Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

[release/3.0] Fix the max token size threshold to correctly compute to 125MB for Base64 bytes.#40796

Merged
ahsonkhan merged 1 commit intodotnet:release/3.0from
ahsonkhan:PortBase64ThresholdFix
Sep 5, 2019
Merged

[release/3.0] Fix the max token size threshold to correctly compute to 125MB for Base64 bytes.#40796
ahsonkhan merged 1 commit intodotnet:release/3.0from
ahsonkhan:PortBase64ThresholdFix

Conversation

@ahsonkhan
Copy link

Ports #40792 to 3.0

Fixes https://github.com/dotnet/corefx/issues/40755

cc @lauxjpn, @steveharter, @scalablecory, @stephentoub, @ericstj

Description

Fix the typo in threshold calculation where rather than doing (1_000_000_000/2^2) * 3 ( == 750_000_000), we were accidentally doing 1_000_000_000/(2^(2 * 3)) (== 15_625_000) because the bracket was misplaced. Turns out, C# ordering of mathematical operations has multiplication first before bit-shifts, but we want to do bit-shifts first. Also, rename the constant to fix transpose error: Base46 -> Base64.

Customer Impact

The bug was customer-reported where the user was unable to return a 3.5MB byte[] from a controller's action within an aspnet app. Effectively, trying to write a byte[] as Base64-encoded string that is > 2.5MB wouldn't work, which falls within the "reasonable" size for binary payload. The actual token size limit was intended to be ~125 MB, which customers have not run up against (this limitation is due to the way Utf8JsonWriter is implemented). Direct users of the Utf8JsonWriter have a sub-optimal workaround for this bug, where they could explicitly encode the byte[] to Base64 string themselves, and pass that to Utf8JsonWriter.WriteString(). However, most folks would hit this via the JsonSerializer or from within an asp.net app, where they may not be able to modify their object model from byte[] to string (encoding to base64 up-front themselves), and there is no workaround for them.

ArgumentException: The JSON value of length 3145728 is too large and not supported.
System.Text.Json.ThrowHelper.ThrowArgumentException_ValueTooLarge(int tokenLength)
System.Text.Json.Utf8JsonWriter.WriteBase64StringValue(ReadOnlySpan<byte> bytes)
System.Text.Json.Serialization.Converters.JsonConverterByteArray.Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
System.Text.Json.JsonPropertyInfoNotNullable<TClass, TDeclaredProperty, TRuntimeProperty, TConverter>.OnWrite(ref WriteStackFrame current, Utf8JsonWriter writer)
System.Text.Json.JsonPropertyInfo.Write(ref WriteStack state, Utf8JsonWriter writer)
System.Text.Json.JsonSerializer.Write(Utf8JsonWriter writer, int originalWriterDepth, int flushThreshold, JsonSerializerOptions options, ref WriteStack state)
System.Text.Json.JsonSerializer.WriteAsyncCore(Stream utf8Json, object value, Type type, JsonSerializerOptions options, CancellationToken cancellationToken)
Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0<TFilter, TFilterAsync>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

image

Regression?

No.

Risk

Utf8JsonWriter.WriteBase64String(...) breaks. Given how isolated the fix is (just a single line - math operation ordering corrected), the risk of this change having unintentional side effects is low. Previously, any caller trying to write large binary data as Base64 string would have hit an exception (which is fixed now) and most callers wouldn't hit this issue. There is a workaround when using Utf8JsonWriter directly. However, since there isn't a workaround for folks using the JsonSerializer or returning byte[] within an ASP.NET app, this certainly meets the servicing bar and given the code change, I believe this should be ported.

Tests run / added

  • An explicit test for writing 3.5MB byte[] via the writer verifying that writing large binary data as Base64 works as expected.
  • Add outerloop tests for writing larger arrays (right up to the 125 MB threshold) and augment existing tests to validate that anything just above 125MB continues to fail as expected (not just really large arrays).
  • Verified end-to-end in an ASP.NET MVC app that returning reasonably sized byte[] work as expected (and outputs base64 encoded strings).

…se64 bytes. (dotnet#40792)

* Fix the max token size threshold to correctly compute to 125MB for
Base64 bytes.

* Rename constant to fix transpose error: Base46 -> Base64

* Enable the outerloop tests for windows and osx only and update to use
platform specific new line.
@ahsonkhan ahsonkhan added this to the 3.0 milestone Sep 4, 2019
@ahsonkhan ahsonkhan self-assigned this Sep 4, 2019
@ahsonkhan
Copy link
Author

Since this was approved, merging.

cc @ericstj

@ahsonkhan ahsonkhan merged commit 4ce4430 into dotnet:release/3.0 Sep 5, 2019
@ahsonkhan ahsonkhan deleted the PortBase64ThresholdFix branch September 5, 2019 04:14
@steveharter
Copy link
Contributor

Fix the typo in threshold calculation where rather than doing (1_000_000_000/2^2) * 3 ( == 750_000_000), we were accidentally doing 1_000_000_000/(2^(2 * 3)) (== 15_625_000) because the bracket was misplaced

The limits were actually 2_604_166 (~2.5MB) and with the parens in the correct place the limit is 125_000_000 (125MB)

old: 2_604_166 == (1_000_000_000 >> 6) / 6) == (1_000_000_000 / 384)
new: 125_000_000 == 1_000_000_000 / 4 * 3 / 6 == 1_000_000_000 / 8

Example

            var buffer = new System.Buffers.ArrayBufferWriter<byte>();
            var writer = new Utf8JsonWriter(buffer);
            writer.WriteStartArray();
            
            // this works
            byte[] bytes = Encoding.UTF8.GetBytes(new String('a', 2_604_166));
            writer.WriteBase64StringValue(bytes);

            // WriteBase64StringValue throws below (before fix)
            byte[] bytes = Encoding.UTF8.GetBytes(new String('a', 2_604_167));
            writer.WriteBase64StringValue(bytes);

@ahsonkhan
Copy link
Author

The limits were actually 2_604_166 (~2.5MB) and with the parens in the correct place the limit is 125_000_000 (125MB)

Yes, of course. Thanks for providing that detail @steveharter and making it clear.

My description was focusing on precisely what changed and the typo in isolation, and was not attempting to show the result of the whole calculation (excluded the division by 6 for brevity).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants