From cda7e381deb3e32b8ccd79a92088f634335a95e3 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Tue, 16 Jun 2026 08:40:05 +0100 Subject: [PATCH] WebContent: Read WebDriver file uploads in the unsandboxed UI process Previously, uploading files with the "Element Send Keys" Webdriver command failed when running with the `--enable-sandbox` flag because the Landlock policy denies access to arbitrary file paths. We now read these files from the unsandboxed UI process in the same way as normal interactive uploads --- Services/WebContent/WebDriverConnection.cpp | 62 ++++++++++++--------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/Services/WebContent/WebDriverConnection.cpp b/Services/WebContent/WebDriverConnection.cpp index 802c2f9f6c..649b466121 100644 --- a/Services/WebContent/WebDriverConnection.cpp +++ b/Services/WebContent/WebDriverConnection.cpp @@ -22,6 +22,7 @@ #else # include #endif +#include #include #include #include @@ -59,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -2037,34 +2039,44 @@ Web::WebDriver::Response WebDriverConnection::element_send_keys_impl(StringView // 5. Verify that each file given by the user exists. If any do not, return error with error code invalid argument. // 6. Complete implementation specific steps equivalent to setting the selected files on the input element. If // multiple is true files are be appended to element's selected files. - auto create_selected_file = [](auto const& path) -> ErrorOr { - auto file = TRY(Core::File::open(path, Core::File::OpenMode::Read)); - auto contents = TRY(file->read_until_eof()); + // NB: Each file is opened in the unsandboxed UI process, because the WebContent sandbox blocks this + // process from opening arbitrary paths. + auto read_files_and_apply_selection = [connection = this, input_element = GC::make_root(input_element)](this auto const& self, Vector paths, size_t index, Vector selected_files) -> void { + if (index < paths.size()) { + auto path = paths[index].to_byte_string(); - return Web::HTML::SelectedFile { LexicalPath::basename(path), move(contents) }; + Web::FileRequest file_request(path, [connection, self, paths = move(paths), index, selected_files = move(selected_files), path](ErrorOr file_descriptor_or_error) mutable { + auto contents_or_error = [&]() -> ErrorOr { + auto opened_file = TRY(Core::File::adopt_fd(TRY(file_descriptor_or_error), Core::File::OpenMode::Read)); + return opened_file->read_until_eof(); + }(); + + if (contents_or_error.is_error()) { + connection->async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, MUST(String::formatted("'{}' does not exist", path)))); + return; + } + + selected_files.append(Web::HTML::SelectedFile { LexicalPath::basename(path), contents_or_error.release_value() }); + self(move(paths), index + 1, move(selected_files)); + }); + + connection->current_browsing_context().page().client().request_file(move(file_request)); + return; + } + + input_element->did_select_files(selected_files, Web::HTML::HTMLInputElement::MultipleHandling::Append); + + // 7. Fire these events in order on element: + // 1. input + // 2. change + // NOTE: These events are fired by `did_select_files` as an element task. So instead of firing them here, we + // spin the event loop once before informing the client that the action is complete. + Web::HTML::queue_a_task(Web::HTML::Task::Source::Unspecified, nullptr, nullptr, GC::create_function(connection->current_browsing_context().heap(), [connection]() { + connection->async_driver_execution_complete(JsonValue {}); + })); }; - Vector selected_files; - selected_files.ensure_capacity(files.size()); - - for (auto const& path : files) { - auto selected_file = create_selected_file(path.bytes_as_string_view()); - if (selected_file.is_error()) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, MUST(String::formatted("'{}' does not exist", path))); - - selected_files.unchecked_append(selected_file.release_value()); - } - - input_element.did_select_files(selected_files, Web::HTML::HTMLInputElement::MultipleHandling::Append); - - // 7. Fire these events in order on element: - // 1. input - // 2. change - // NOTE: These events are fired by `did_select_files` as an element task. So instead of firing them here, we spin - // the event loop once before informing the client that the action is complete. - Web::HTML::queue_a_task(Web::HTML::Task::Source::Unspecified, nullptr, nullptr, GC::create_function(current_browsing_context().heap(), [this]() { - async_driver_execution_complete(JsonValue {}); - })); + read_files_and_apply_selection(move(files), 0, {}); // 8. Return success with data null. return JsonValue {};