From 90620d3cda4f6502c25e1e2ce025f7618e7b4793 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Mon, 29 Jun 2026 20:54:24 +0800 Subject: [PATCH 1/2] Use simdutf for remaining base64 decodes --- src/workerd/api/BUILD.bazel | 2 +- src/workerd/api/data-url.c++ | 40 ++++++++++++++++++++++++++++---- src/workerd/api/global-scope.c++ | 12 ++++++---- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/workerd/api/BUILD.bazel b/src/workerd/api/BUILD.bazel index e548f11c99d..b56993485ce 100644 --- a/src/workerd/api/BUILD.bazel +++ b/src/workerd/api/BUILD.bazel @@ -455,7 +455,6 @@ wd_cc_library( srcs = ["data-url.c++"], hdrs = ["data-url.h"], implementation_deps = [ - ":encoding", "//src/workerd/util:strings", ], visibility = ["//visibility:public"], @@ -463,6 +462,7 @@ wd_cc_library( "//src/workerd/jsg:url", "//src/workerd/util:mimetype", "@capnp-cpp//src/kj", + "@simdutf", ], ) diff --git a/src/workerd/api/data-url.c++ b/src/workerd/api/data-url.c++ index 0fe648f202c..5ea494df9a1 100644 --- a/src/workerd/api/data-url.c++ +++ b/src/workerd/api/data-url.c++ @@ -1,11 +1,44 @@ #include "data-url.h" -#include +#include "simdutf.h" + #include -#include +#include namespace workerd::api { +namespace { + +kj::Array decodeDataUrlBase64(kj::ArrayPtr input) { + kj::Vector filtered(input.size()); + + for (kj::byte c: input) { + if (isAlpha(c) || isDigit(c) || c == '+' || c == '/') { + filtered.add(static_cast(c)); + } + } + + auto base64 = filtered.asPtr(); + if (base64.size() % 4 == 1) { + base64 = base64.first(base64.size() - 1); + } + + if (base64.size() == 0) { + return nullptr; + } + + auto size = simdutf::maximal_binary_length_from_base64(base64.begin(), base64.size()); + auto decoded = kj::heapArray(size); + auto result = simdutf::base64_to_binary( + base64.begin(), base64.size(), decoded.asChars().begin(), simdutf::base64_default); + if (result.error != simdutf::SUCCESS || result.count == 0) { + return nullptr; + } + KJ_ASSERT(result.count <= size); + return decoded.first(result.count).attach(kj::mv(decoded)); +} + +} // namespace kj::Maybe DataUrl::tryParse(kj::StringPtr url) { KJ_IF_SOME(url, jsg::Url::tryParse(url)) { @@ -53,8 +86,7 @@ kj::Maybe DataUrl::from(const jsg::Url& url) { kj::Array decoded = nullptr; if (isBase64(unparsed)) { unparsed = unparsed.first(KJ_ASSERT_NONNULL(unparsed.findLast(';'))); - decoded = - kj::decodeBase64(stripInnerWhitespace(jsg::Url::percentDecode(data.asBytes())).asChars()); + decoded = decodeDataUrlBase64(jsg::Url::percentDecode(data.asBytes())); } else { decoded = jsg::Url::percentDecode(data.asBytes()); } diff --git a/src/workerd/api/global-scope.c++ b/src/workerd/api/global-scope.c++ index 434ea9c3236..53b7763d5f0 100644 --- a/src/workerd/api/global-scope.c++ +++ b/src/workerd/api/global-scope.c++ @@ -928,17 +928,21 @@ void ServiceWorkerGlobalScope::fuzzilli(jsg::Lock& js, jsg::Arguments(size); + auto result = simdutf::base64_to_binary( + data.begin(), data.size(), decoded.asChars().begin(), simdutf::base64_default); - JSG_REQUIRE(!decoded.hadErrors, DOMInvalidCharacterError, + JSG_REQUIRE(result.error == simdutf::SUCCESS, DOMInvalidCharacterError, "atob() called with invalid base64-encoded data. (Only whitespace, '+', '/', alphanumeric " "ASCII, and up to two terminal '=' signs when the input data length is divisible by 4 are " "allowed.)"); // Similar to btoa() taking a v8::Value, we return a v8::String directly, as this allows us to - // construct a string from the non-nul-terminated array returned from decodeBase64(). This avoids + // construct a string from the non-nul-terminated array returned from base64_to_binary(). This avoids // making a copy purely to append a nul byte. - return js.str(decoded.asBytes()); + KJ_ASSERT(result.count <= size); + return js.str(decoded.first(result.count)); } void ServiceWorkerGlobalScope::queueMicrotask(jsg::Lock& js, jsg::Function task) { From 40c939235942804cdd95b3a5af0e27f7fad2bb9f Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 7 Jul 2026 10:15:15 +0800 Subject: [PATCH 2/2] Document DataUrl base64 compatibility behavior --- src/workerd/api/data-url.c++ | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/workerd/api/data-url.c++ b/src/workerd/api/data-url.c++ index 5ea494df9a1..11b3d264682 100644 --- a/src/workerd/api/data-url.c++ +++ b/src/workerd/api/data-url.c++ @@ -12,6 +12,9 @@ namespace { kj::Array decodeDataUrlBase64(kj::ArrayPtr input) { kj::Vector filtered(input.size()); + // DataUrl historically ignored kj::decodeBase64().hadErrors. That decoder skips bytes outside + // the base64 alphabet, including padding, and decodes the remaining characters. Preserve that + // permissive behavior before passing the input to simdutf's stricter decoder. for (kj::byte c: input) { if (isAlpha(c) || isDigit(c) || c == '+' || c == '/') { filtered.add(static_cast(c)); @@ -19,6 +22,8 @@ kj::Array decodeDataUrlBase64(kj::ArrayPtr input) { } auto base64 = filtered.asPtr(); + // KJ drops a final lone base64 character because it cannot produce a complete byte. simdutf + // rejects inputs whose length is 1 modulo 4, so trim that character to preserve KJ's result. if (base64.size() % 4 == 1) { base64 = base64.first(base64.size() - 1); } @@ -29,6 +34,8 @@ kj::Array decodeDataUrlBase64(kj::ArrayPtr input) { auto size = simdutf::maximal_binary_length_from_base64(base64.begin(), base64.size()); auto decoded = kj::heapArray(size); + // The default loose tail handling preserves KJ's acceptance of unpadded two- and three-character + // tails and non-zero unused bits. auto result = simdutf::base64_to_binary( base64.begin(), base64.size(), decoded.asChars().begin(), simdutf::base64_default); if (result.error != simdutf::SUCCESS || result.count == 0) {