| // Copyright 2026 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/developer/debug/zxdb/debug_adapter/handlers/request_data_breakpoint.h" |
| |
| #include <inttypes.h> |
| #include <lib/fit/defer.h> |
| #include <lib/fit/function.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <limits> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "src/developer/debug/shared/arch.h" |
| #include "src/developer/debug/zxdb/client/breakpoint.h" |
| #include "src/developer/debug/zxdb/client/breakpoint_settings.h" |
| #include "src/developer/debug/zxdb/client/execution_scope.h" |
| #include "src/developer/debug/zxdb/client/frame.h" |
| #include "src/developer/debug/zxdb/client/process.h" |
| #include "src/developer/debug/zxdb/client/session.h" |
| #include "src/developer/debug/zxdb/client/system.h" |
| #include "src/developer/debug/zxdb/client/target.h" |
| #include "src/developer/debug/zxdb/client/thread.h" |
| #include "src/developer/debug/zxdb/common/err_or.h" |
| #include "src/developer/debug/zxdb/expr/expr.h" |
| #include "src/developer/debug/zxdb/expr/expr_value.h" |
| #include "src/developer/debug/zxdb/expr/format.h" |
| #include "src/developer/debug/zxdb/expr/format_node.h" |
| #include "src/developer/debug/zxdb/expr/resolve_ptr_ref.h" |
| #include "src/developer/debug/zxdb/symbols/function.h" |
| #include "src/developer/debug/zxdb/symbols/variable.h" |
| #include "src/lib/fxl/strings/string_number_conversions.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| |
| namespace zxdb { |
| |
| namespace { |
| |
| // Represents the DAP `dataId` identifier exchanged between `dataBreakpointInfo` and |
| // `setDataBreakpoints` requests. |
| // |
| // Format: |
| // "<process_koid>:0x<address_hex>:<byte_size>" |
| // Example: "17601:0x7ffee1234000:4" |
| // |
| // Fields & Invariants: |
| // - `process_koid`: Base-10 Zircon KOID of the target process. Must be non-zero |
| // (`ZX_KOID_INVALID` == 0 is rejected). Because virtual memory addresses are only meaningful |
| // within the address space of the process in which the expression was evaluated, a valid |
| // process KOID is always required so the watchpoint is scoped strictly to that process. |
| // - `address`: Base-16 virtual address prefixed with "0x" or "0X". Must not overflow the 64-bit |
| // address space when combined with `byte_size` (`address <= UINT64_MAX - byte_size`). |
| // - `byte_size`: Base-10 size in bytes of the watched memory location. Must be supported by the |
| // target architecture's hardware debug registers (`BreakpointSettings::ValidateSize`). |
| // |
| // Usage: |
| // - In `dataBreakpointInfo`: Construct via `DataId::FromExprValue(...)` and serialize to the DAP |
| // response using `ToString()`. |
| // - In `setDataBreakpoints`: Parse and validate the client-supplied string via |
| // `DataId::Parse(...)` and validate hardware size support via `ValidateSize(...)`. |
| class DataId { |
| public: |
| // Evaluates whether `value` can be watched and returns a validated `DataId`, or an `Err` |
| // describing why a data breakpoint cannot be set on `value`. Only properties of the value itself |
| // are checked here; `process_koid` must already be a valid (non-zero) KOID. |
| static ErrOr<DataId> FromExprValue(debug::Arch arch, uint64_t process_koid, |
| const std::string& name, const ExprValue& value); |
| |
| // Parses a `<process_koid>:0x<address_hex>:<byte_size>` string and checks format, non-zero |
| // `process_koid`, and 64-bit address range overflow invariants. |
| static ErrOr<DataId> Parse(std::string_view data_id); |
| |
| // Validates that `byte_size()` is supported by `arch` for the given hardware watchpoint `type`. |
| Err ValidateSize(debug::Arch arch, BreakpointSettings::Type type) const; |
| |
| // Formats this `DataId` into its canonical string representation. |
| std::string ToString() const; |
| |
| uint64_t process_koid() const { return process_koid_; } |
| uint64_t address() const { return address_; } |
| uint32_t byte_size() const { return byte_size_; } |
| |
| private: |
| DataId(uint64_t process_koid, uint64_t address, uint32_t byte_size) |
| : process_koid_(process_koid), address_(address), byte_size_(byte_size) {} |
| |
| uint64_t process_koid_ = 0; |
| uint64_t address_ = 0; |
| uint32_t byte_size_ = 0; |
| }; |
| |
| ErrOr<DataId> DataId::FromExprValue(debug::Arch arch, uint64_t process_koid, |
| const std::string& name, const ExprValue& value) { |
| FX_DCHECK(process_koid != 0); |
| |
| const ExprValueSource& source = value.source(); |
| if (source.type() != ExprValueSource::Type::kMemory) { |
| return Err( |
| "Expression '%s' is stored in a %s location. Only values stored in memory can be watched.", |
| name.c_str(), ExprValueSource::TypeToString(source.type())); |
| } |
| |
| if (source.is_bitfield()) { |
| return Err("Bitfields cannot be watched."); |
| } |
| |
| size_t raw_size = value.data().size(); |
| uint32_t size = raw_size > std::numeric_limits<uint32_t>::max() |
| ? std::numeric_limits<uint32_t>::max() |
| : static_cast<uint32_t>(raw_size); |
| if (Err err = BreakpointSettings::ValidateSize(arch, BreakpointSettings::Type::kWrite, size); |
| err.has_error()) { |
| return err; |
| } |
| |
| uint64_t address = source.address(); |
| if (address > std::numeric_limits<uint64_t>::max() - size) { |
| return Err("Address range overflows 64-bit address space."); |
| } |
| |
| return DataId(process_koid, address, size); |
| } |
| |
| ErrOr<DataId> DataId::Parse(std::string_view data_id) { |
| Err format_err("Invalid dataId format: " + std::string(data_id)); |
| |
| size_t first_colon = data_id.find(':'); |
| if (first_colon == std::string_view::npos) { |
| return format_err; |
| } |
| |
| size_t second_colon = data_id.find(':', first_colon + 1); |
| if (second_colon == std::string_view::npos || |
| data_id.find(':', second_colon + 1) != std::string_view::npos) { |
| return format_err; |
| } |
| |
| uint64_t koid = 0; |
| if (!fxl::StringToNumberWithError<uint64_t>(data_id.substr(0, first_colon), &koid, |
| fxl::Base::k10) || |
| koid == 0) { |
| return format_err; |
| } |
| |
| std::string_view addr_part = data_id.substr(first_colon + 1, second_colon - first_colon - 1); |
| std::string_view size_part = data_id.substr(second_colon + 1); |
| |
| if (addr_part.size() <= 2 || addr_part[0] != '0' || |
| (addr_part[1] != 'x' && addr_part[1] != 'X')) { |
| return format_err; |
| } |
| |
| uint64_t address = 0; |
| uint32_t byte_size = 0; |
| if (!fxl::StringToNumberWithError<uint64_t>(addr_part.substr(2), &address, fxl::Base::k16) || |
| !fxl::StringToNumberWithError<uint32_t>(size_part, &byte_size, fxl::Base::k10)) { |
| return format_err; |
| } |
| |
| if (address > std::numeric_limits<uint64_t>::max() - byte_size) { |
| return format_err; |
| } |
| |
| return DataId(koid, address, byte_size); |
| } |
| |
| Err DataId::ValidateSize(debug::Arch arch, BreakpointSettings::Type type) const { |
| return BreakpointSettings::ValidateSize(arch, type, byte_size_); |
| } |
| |
| std::string DataId::ToString() const { |
| return fxl::StringPrintf("%" PRIu64 ":0x%" PRIx64 ":%u", process_koid_, address_, byte_size_); |
| } |
| |
| using DataBreakpointInfoCallback = |
| fit::function<void(dap::ResponseOrError<dap::DataBreakpointInfoResponse>)>; |
| |
| void ProcessEvaluatedValue(const fxl::WeakPtr<DebugAdapterContext>& weak_ctx, uint64_t process_koid, |
| const std::string& name, const ErrOrValue& result, |
| DataBreakpointInfoCallback callback) { |
| // Every path out of this function must invoke `callback`. The DAP session only writes a response |
| // when the callback is called, so dropping it leaves the client waiting forever. |
| if (!weak_ctx) { |
| callback(dap::Error("Debug adapter context was destroyed.")); |
| return; |
| } |
| |
| if (result.has_error()) { |
| callback(dap::Error(result.err().msg())); |
| return; |
| } |
| |
| ErrOr<DataId> data_id = |
| DataId::FromExprValue(weak_ctx->session()->arch(), process_koid, name, result.value()); |
| if (data_id.has_error()) { |
| dap::DataBreakpointInfoResponse response; |
| response.dataId = dap::null(); |
| response.description = data_id.err().msg(); |
| callback(response); |
| return; |
| } |
| |
| dap::DataBreakpointInfoResponse response; |
| response.dataId = data_id.value().ToString(); |
| response.description = fxl::StringPrintf("%s (0x%" PRIx64 ", %u bytes)", name.c_str(), |
| data_id.value().address(), data_id.value().byte_size()); |
| response.accessTypes = dap::array<dap::DataBreakpointAccessType>{"write", "readWrite"}; |
| response.canPersist = false; |
| callback(response); |
| } |
| |
| void ResolveChildVariable(DebugAdapterContext* ctx, VariablesRecord* record, Frame* frame, |
| uint64_t process_koid, const std::string& name, |
| DataBreakpointInfoCallback callback) { |
| FormatNode* parent_node = record->parent ? record->parent.get() : record->child.get(); |
| if (!parent_node) { |
| callback(dap::Error("No node pointer for variable.")); |
| return; |
| } |
| FormatNode* target_child = nullptr; |
| for (const auto& child : parent_node->children()) { |
| if (child->name() == name) { |
| target_child = child.get(); |
| break; |
| } |
| } |
| if (!target_child) { |
| callback(dap::Error(fxl::StringPrintf("Child variable '%s' not found.", name.c_str()))); |
| return; |
| } |
| |
| if (target_child->state() == FormatNode::kUnevaluated) { |
| auto weak_child = target_child->GetWeakPtr(); |
| auto weak_ctx = ctx->GetWeakPtr(); |
| auto eval_ctx = frame->GetEvalContext(); |
| FillFormatNodeValue( |
| target_child, eval_ctx, |
| fit::defer_callback([weak_ctx, weak_child, eval_ctx, process_koid, name, |
| cb = std::move(callback)]() mutable { |
| if (!weak_ctx || !weak_child) { |
| cb(dap::Error("Variable went out of scope while it was being evaluated.")); |
| return; |
| } |
| if (weak_child->err().has_error()) { |
| ProcessEvaluatedValue(weak_ctx, process_koid, name, weak_child->err(), std::move(cb)); |
| return; |
| } |
| EnsureResolveReference( |
| eval_ctx, weak_child->value(), |
| [weak_ctx, process_koid, name, cb = std::move(cb)](const ErrOrValue& val) mutable { |
| ProcessEvaluatedValue(weak_ctx, process_koid, name, val, std::move(cb)); |
| }); |
| })); |
| return; |
| } |
| |
| if (target_child->err().has_error()) { |
| ProcessEvaluatedValue(ctx->GetWeakPtr(), process_koid, name, target_child->err(), |
| std::move(callback)); |
| } else { |
| EnsureResolveReference(frame->GetEvalContext(), target_child->value(), |
| [weak_ctx = ctx->GetWeakPtr(), process_koid, name, |
| cb = std::move(callback)](const ErrOrValue& val) mutable { |
| ProcessEvaluatedValue(weak_ctx, process_koid, name, val, |
| std::move(cb)); |
| }); |
| } |
| } |
| |
| void ResolveFunctionParameter(DebugAdapterContext* ctx, Frame* frame, uint64_t process_koid, |
| const std::string& name, DataBreakpointInfoCallback callback) { |
| const Location& location = frame->GetLocation(); |
| if (!location.symbol()) { |
| callback(dap::Error("There is no symbol information for the frame.")); |
| return; |
| } |
| const Function* function = location.symbol().Get()->As<Function>(); |
| if (!function) { |
| callback(dap::Error("Symbols are corrupt.")); |
| return; |
| } |
| fxl::RefPtr<Variable> target_param; |
| if (!name.empty()) { |
| for (const auto& param : function->parameters()) { |
| const Variable* var = param.Get()->As<Variable>(); |
| if (var && var->GetAssignedName() == name) { |
| target_param = RefPtrTo(var); |
| break; |
| } |
| } |
| } |
| if (!target_param) { |
| callback(dap::Error(fxl::StringPrintf("Function parameter '%s' not found.", name.c_str()))); |
| return; |
| } |
| auto eval_ctx = frame->GetEvalContext(); |
| eval_ctx->GetVariableValue( |
| std::move(target_param), [weak_ctx = ctx->GetWeakPtr(), eval_ctx, process_koid, name, |
| cb = std::move(callback)](const ErrOrValue& result) mutable { |
| if (result.has_error()) { |
| ProcessEvaluatedValue(weak_ctx, process_koid, name, result, std::move(cb)); |
| return; |
| } |
| EnsureResolveReference( |
| eval_ctx, result.value(), |
| [weak_ctx, process_koid, name, cb = std::move(cb)](const ErrOrValue& resolved) mutable { |
| ProcessEvaluatedValue(weak_ctx, process_koid, name, resolved, std::move(cb)); |
| }); |
| }); |
| } |
| |
| // Returns the KOID of the process owning `frame`'s thread, or an `Err` if the thread is not |
| // stopped. |
| ErrOr<uint64_t> ProcessKoidForStoppedFrame(DebugAdapterContext* ctx, Frame* frame) { |
| if (Err err = ctx->CheckStoppedThread(frame->GetThread()); err.has_error()) { |
| return err; |
| } |
| Process* process = frame->GetThread()->GetProcess(); |
| FX_DCHECK(process); |
| uint64_t koid = process->GetKoid(); |
| FX_DCHECK(koid != 0); |
| return koid; |
| } |
| |
| void HandleVariablesReferenceRequest(DebugAdapterContext* ctx, |
| const dap::DataBreakpointInfoRequest& req, |
| DataBreakpointInfoCallback callback) { |
| auto* record = ctx->VariablesRecordForID(req.variablesReference.value()); |
| if (!record) { |
| callback(dap::Error("Invalid variables reference.")); |
| return; |
| } |
| auto* frame = ctx->FrameforId(record->frame_id); |
| if (!frame) { |
| callback(dap::Error("Invalid frame for variables reference.")); |
| return; |
| } |
| ErrOr<uint64_t> koid_or = ProcessKoidForStoppedFrame(ctx, frame); |
| if (koid_or.has_error()) { |
| callback(dap::Error(koid_or.err().msg())); |
| return; |
| } |
| uint64_t process_koid = koid_or.value(); |
| |
| switch (record->type) { |
| case VariablesType::kRegister: { |
| dap::DataBreakpointInfoResponse response; |
| response.dataId = dap::null(); |
| response.description = "Registers cannot be watched."; |
| callback(response); |
| return; |
| } |
| case VariablesType::kChildVariable: |
| ResolveChildVariable(ctx, record, frame, process_koid, req.name, std::move(callback)); |
| return; |
| case VariablesType::kArguments: |
| ResolveFunctionParameter(ctx, frame, process_koid, req.name, std::move(callback)); |
| return; |
| case VariablesType::kLocal: |
| EvalExpression(req.name, frame->GetEvalContext(), /*follow_references=*/true, |
| [weak_ctx = ctx->GetWeakPtr(), process_koid, name = req.name, |
| cb = std::move(callback)](const ErrOrValue& result) mutable { |
| ProcessEvaluatedValue(weak_ctx, process_koid, name, result, std::move(cb)); |
| }); |
| return; |
| case VariablesType::kVariablesTypeCount: |
| break; |
| } |
| callback(dap::Error("Invalid variables type.")); |
| } |
| |
| void HandleFrameExpressionRequest(DebugAdapterContext* ctx, |
| const dap::DataBreakpointInfoRequest& req, |
| DataBreakpointInfoCallback callback) { |
| auto* frame = ctx->FrameforId(req.frameId.value()); |
| if (!frame) { |
| callback(dap::Error("Invalid frame ID.")); |
| return; |
| } |
| ErrOr<uint64_t> koid_or = ProcessKoidForStoppedFrame(ctx, frame); |
| if (koid_or.has_error()) { |
| callback(dap::Error(koid_or.err().msg())); |
| return; |
| } |
| uint64_t process_koid = koid_or.value(); |
| EvalExpression(req.name, frame->GetEvalContext(), /*follow_references=*/true, |
| [weak_ctx = ctx->GetWeakPtr(), process_koid, name = req.name, |
| cb = std::move(callback)](const ErrOrValue& result) mutable { |
| ProcessEvaluatedValue(weak_ctx, process_koid, name, result, std::move(cb)); |
| }); |
| } |
| |
| } // namespace |
| |
| void OnRequestDataBreakpointInfo( |
| DebugAdapterContext* ctx, const dap::DataBreakpointInfoRequest& req, |
| fit::function<void(dap::ResponseOrError<dap::DataBreakpointInfoResponse>)> callback) { |
| if (req.variablesReference.has_value() && req.variablesReference.value() != 0) { |
| HandleVariablesReferenceRequest(ctx, req, std::move(callback)); |
| } else if (req.frameId.has_value()) { |
| HandleFrameExpressionRequest(ctx, req, std::move(callback)); |
| } else { |
| callback(dap::Error("Either variablesReference or frameId must be specified.")); |
| } |
| } |
| |
| dap::ResponseOrError<dap::SetDataBreakpointsResponse> OnRequestSetDataBreakpoints( |
| DebugAdapterContext* ctx, const dap::SetDataBreakpointsRequest& req) { |
| // TODO(https://fxbug.dev/532512768): Diff requested data breakpoints against existing ones so |
| // unchanged watchpoints in running processes are not removed and reinstalled. |
| ctx->DeleteAllDataBreakpoints(); |
| |
| dap::SetDataBreakpointsResponse response; |
| for (const auto& request_bp : req.breakpoints) { |
| ErrOr<DataId> data_id = DataId::Parse(request_bp.dataId); |
| if (data_id.has_error()) { |
| response.breakpoints.push_back(dap::Breakpoint{ |
| .message = data_id.err().msg(), |
| .verified = false, |
| }); |
| continue; |
| } |
| |
| BreakpointSettings::Type type = BreakpointSettings::Type::kWrite; |
| if (request_bp.accessType.has_value()) { |
| const std::string& access = request_bp.accessType.value(); |
| if (access == "write") { |
| type = BreakpointSettings::Type::kWrite; |
| } else if (access == "readWrite") { |
| type = BreakpointSettings::Type::kReadWrite; |
| } else if (access == "read") { |
| response.breakpoints.push_back(dap::Breakpoint{ |
| .message = "Read-only data breakpoints are not supported on this platform.", |
| .verified = false, |
| }); |
| continue; |
| } else { |
| response.breakpoints.push_back(dap::Breakpoint{ |
| .message = "Unknown accessType: " + access, |
| .verified = false, |
| }); |
| continue; |
| } |
| } |
| |
| if (request_bp.hitCondition.has_value()) { |
| response.breakpoints.push_back(dap::Breakpoint{ |
| .message = "Hit conditions are not supported for data breakpoints.", |
| .verified = false, |
| }); |
| continue; |
| } |
| |
| if (Err err = data_id.value().ValidateSize(ctx->session()->arch(), type); err.has_error()) { |
| response.breakpoints.push_back(dap::Breakpoint{ |
| .message = err.msg(), |
| .verified = false, |
| }); |
| continue; |
| } |
| |
| // The dataId encodes a raw address, which is only meaningful within the address space it was |
| // evaluated in. Scope the watchpoint strictly to the process identified by `process_koid()`. |
| Process* process = ctx->session()->system().ProcessFromKoid(data_id.value().process_koid()); |
| if (!process || !process->GetTarget()) { |
| response.breakpoints.push_back(dap::Breakpoint{ |
| .message = "No active process to set a data breakpoint on.", |
| .verified = false, |
| }); |
| continue; |
| } |
| Target* target = process->GetTarget(); |
| |
| Breakpoint* breakpoint = ctx->session()->system().CreateNewBreakpoint(); |
| BreakpointSettings settings; |
| settings.type = type; |
| settings.byte_size = data_id.value().byte_size(); |
| settings.scope = ExecutionScope(target); |
| settings.locations = {InputLocation(data_id.value().address())}; |
| if (request_bp.condition.has_value()) { |
| settings.condition = request_bp.condition.value(); |
| } |
| breakpoint->SetSettings(settings); |
| ctx->StoreDataBreakpoint(breakpoint); |
| |
| response.breakpoints.push_back(dap::Breakpoint{ |
| .id = ctx->IdForBreakpoint(breakpoint), |
| .verified = (!breakpoint->GetLocations().empty()), |
| }); |
| } |
| |
| return response; |
| } |
| |
| } // namespace zxdb |