diff --git a/_fixtures/readmem_json.go b/_fixtures/readmem_json.go new file mode 100644 index 0000000000..857fa4e50d --- /dev/null +++ b/_fixtures/readmem_json.go @@ -0,0 +1,43 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "runtime" + "strings" + "unsafe" +) + +// Long json for MemoryReference +func main() { + runtime.Breakpoint() + + payload := strings.Repeat("AB", 2500) + + b, _ := json.Marshal(struct { + Data string `json:"data"` + }{Data: payload}) + + bytesString := []byte("this\nis\nit") + nonprint := []byte{242, 243, 244, 245} + maps := map[string]string{"some": "non"} + + jsonString := string(b) + + hashed := sha256.Sum256(b) + jsonHash := hex.EncodeToString(hashed[:]) // used to validate fullness of a string + + ptr := unsafe.StringData(jsonString) + jsonAddr := fmt.Sprintf("%p", ptr) // used to validate string address + + runtime.Breakpoint() + + fmt.Println(jsonString) + fmt.Println(jsonHash) + fmt.Println(jsonAddr) + fmt.Println(bytesString) + fmt.Println(nonprint) + fmt.Println(maps) +} diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go index d9a457d073..a853cab591 100644 --- a/service/dap/daptest/client.go +++ b/service/dap/daptest/client.go @@ -213,6 +213,7 @@ func (c *Client) InitializeRequest() { SupportsVariableType: true, SupportsVariablePaging: true, SupportsRunInTerminalRequest: true, + SupportsMemoryReferences: true, Locale: "en-us", } c.send(request) @@ -550,8 +551,14 @@ func (c *Client) SetDataBreakpointsRequest() { } // ReadMemoryRequest sends a 'readMemory' request. -func (c *Client) ReadMemoryRequest() { - c.send(&dap.ReadMemoryRequest{Request: *c.newRequest("readMemory")}) +func (c *Client) ReadMemoryRequest(ref string, offset, count int) { + c.send(&dap.ReadMemoryRequest{ + Request: *c.newRequest("readMemory"), + Arguments: dap.ReadMemoryArguments{ + MemoryReference: ref, + Offset: offset, + Count: count, + }}) } // DisassembleRequest sends a 'disassemble' request. diff --git a/service/dap/error_ids.go b/service/dap/error_ids.go index 7222329159..598e225824 100644 --- a/service/dap/error_ids.go +++ b/service/dap/error_ids.go @@ -28,6 +28,7 @@ const ( UnableToDisassemble = 2013 UnableToListRegisters = 2014 UnableToRunDlvCommand = 2015 + UnableToReadMemory = 2016 // Add more codes as we support more requests diff --git a/service/dap/server.go b/service/dap/server.go index 99920cae94..da4f7665dc 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -11,6 +11,7 @@ package dap import ( "bufio" "bytes" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -104,6 +105,71 @@ type Server struct { sessionMu sync.Mutex } +// memRef describe address and its size to stream data from +type memRef struct { + addr uint64 + size int64 +} + +type referencesCollection struct { + mu sync.Mutex + refs map[string]memRef +} + +func (r *referencesCollection) get(reference string) (memRef, bool) { + r.mu.Lock() + defer r.mu.Unlock() + + ref, ok := r.refs[reference] + + return ref, ok +} + +func isAddressable(v *proc.Variable) bool { + if v == nil || v.Unreadable != nil { + return false + } + + switch v.Kind { + case reflect.Slice, reflect.String: + return true + } + + return false +} + +func (r *referencesCollection) put(v *proc.Variable) string { + if !isAddressable(v) { + return "" + } + + r.mu.Lock() + defer r.mu.Unlock() + + if r.refs == nil { + r.refs = make(map[string]memRef) + } + + addr := v.Addr + if v.Base != 0 { + addr = v.Base + } + + ref := fmt.Sprintf("0x%x", addr) + r.refs[ref] = memRef{addr: addr, size: v.Len} + + return ref +} + +func (r *referencesCollection) reset() { + r.mu.Lock() + defer r.mu.Unlock() + + if len(r.refs) > 0 { + r.refs = make(map[string]memRef) + } +} + // Session is an abstraction for serving and shutting down // a DAP debug session with a pre-connected client. // TODO(polina): move this to a different file/package @@ -119,6 +185,8 @@ type Session struct { // Reset at every stop. // See also comment for convertVariable. variableHandles *handlesMap[*fullyQualifiedVariable] + // referencesCollection track references map for DAP client + referencesCollection referencesCollection // args tracks special settings for handling debug session requests. args launchAttachArgs // exceptionErr tracks the runtime error that last occurred. @@ -774,7 +842,7 @@ func (s *Session) handleRequest(request dap.Message) { case *dap.LoadedSourcesRequest: // Optional (capability 'supportsLoadedSourcesRequest') /*TODO*/ s.onLoadedSourcesRequest(request) // Not yet implemented case *dap.ReadMemoryRequest: // Optional (capability 'supportsReadMemoryRequest') - /*TODO*/ s.onReadMemoryRequest(request) // Not yet implemented + s.onReadMemoryRequest(request) case *dap.CancelRequest: // Optional (capability 'supportsCancelRequest') /*TODO*/ s.onCancelRequest(request) // Not yet implemented (does this make sense?) case *dap.ModulesRequest: // Optional (capability 'supportsModulesRequest') @@ -871,7 +939,7 @@ func (s *Session) onInitializeRequest(request *dap.InitializeRequest) { response.Body.SupportsRestartRequest = true response.Body.SupportsSetExpression = false response.Body.SupportsLoadedSourcesRequest = false - response.Body.SupportsReadMemoryRequest = false + response.Body.SupportsReadMemoryRequest = true response.Body.SupportsCancelRequest = false response.Body.ExceptionBreakpointFilters = []dap.ExceptionBreakpointsFilter{ {Filter: proc.UnrecoveredPanic, Label: "Unrecovered Panics", Default: true}, @@ -2656,6 +2724,7 @@ func (s *Session) childrenToDAPVariables(v *fullyQualifiedVariable) []dap.Variab VariablesReference: cvarref, IndexedVariables: getIndexedVariableCount(c), NamedVariables: getNamedVariableCount(c), + MemoryReference: s.referencesCollection.put(c), } } } @@ -3355,10 +3424,92 @@ func (s *Session) onLoadedSourcesRequest(request *dap.LoadedSourcesRequest) { s.sendNotYetImplementedErrorResponse(request.Request) } -// onReadMemoryRequest sends a not-yet-implemented error response. -// Capability 'supportsReadMemoryRequest' is not set 'initialize' response. +// onReadMemoryRequest handles DAP read memory requests func (s *Session) onReadMemoryRequest(request *dap.ReadMemoryRequest) { - s.sendNotYetImplementedErrorResponse(request.Request) + args := request.Arguments + + if args.Count < 0 { + s.sendErrorResponse(request.Request, UnableToReadMemory, "Unable to read memory", "negative count") + return + } + + ref, ok := s.referencesCollection.get(args.MemoryReference) + if !ok { + s.sendErrorResponse(request.Request, UnableToReadMemory, "Unable to read memory", "unknown memoryReference") + return + } + + if args.Count == 0 { + s.send(makeReadMemoryResponse(request.Request, ref.addr, nil, 0)) + return + } + + endReq := int64(args.Offset + args.Count) + + startRead := min(max(int64(args.Offset), 0), ref.size) + endRead := min(max(endReq, 0), ref.size) + + memAddr := ref.addr + uint64(startRead) + + readCount := endRead - startRead + if readCount <= 0 { + unreadable := max(args.Count, 0) + + s.send(makeReadMemoryResponse(request.Request, memAddr, nil, unreadable)) + return + } + + unreadable := max(int64(args.Count)-readCount, 0) + + data, n, err := s.readTargetMemory(memAddr, readCount) + if err != nil { + s.sendErrorResponse(request.Request, UnableToReadMemory, "Unable to read memory", err.Error()) + return + } + + if int64(n) < readCount { + unreadable += readCount - int64(n) + } + + s.send(makeReadMemoryResponse(request.Request, memAddr, data, int(unreadable))) +} + +func (s *Session) readTargetMemory(addr uint64, count int64) (data []byte, n int, err error) { + if count <= 0 { + return nil, 0, nil + } + + buf := make([]byte, count) + + tgrp, unlock := s.debugger.LockTargetGroup() + defer unlock() + + n, err = tgrp.Selected.Memory().ReadMemory(buf, addr) + if err != nil { + return nil, 0, err + } + + if n > 0 { + data = buf[:n] + } + + return data, n, nil +} + +func makeReadMemoryResponse(req dap.Request, addr uint64, data []byte, unreadable int) *dap.ReadMemoryResponse { + var response string + if len(data) > 0 { + response = base64.StdEncoding.EncodeToString(data) + } + + return &dap.ReadMemoryResponse{ + Response: *newResponse(req), + Body: dap.ReadMemoryResponseBody{ + Address: fmt.Sprintf("%#x", addr), + Data: response, + UnreadableBytes: unreadable, + }, + } } var invalidInstruction = dap.DisassembledInstruction{ @@ -3814,6 +3965,7 @@ Use 'Continue' to resume the original step command.` func (s *Session) resetHandlesForStoppedEvent() { s.stackFrameHandles.reset() s.variableHandles.reset() + s.referencesCollection.reset() s.exceptionErr = nil } diff --git a/service/dap/server_test.go b/service/dap/server_test.go index 240a407d6f..9aaa25c119 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -4,6 +4,9 @@ import ( "bufio" "bytes" "cmp" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "flag" "fmt" "io" @@ -6548,9 +6551,6 @@ func TestOptionalNotYetImplementedResponses(t *testing.T) { client.LoadedSourcesRequest() expectNotYetImplemented("loadedSources") - client.ReadMemoryRequest() - expectNotYetImplemented("readMemory") - client.CancelRequest() expectNotYetImplemented("cancel") @@ -8054,6 +8054,130 @@ func TestRedirects(t *testing.T) { }) } +func TestReadMemory_StringPagination(t *testing.T) { + if runtime.GOOS == "freebsd" { + t.Skip("test skipped on freebsd") + } + + runTest(t, "readmem_json", func(client *daptest.Client, fixture protest.Fixture) { + runDebugSessionWithBPs(t, client, "launch", + // Launch + func() { + client.LaunchRequest("exec", fixture.Path, !stopOnEntry) + }, + // Breakpoints are set within the program + fixture.Source, []int{}, + []onBreakpoint{ + { + execute: func() {}, + disconnect: false, + }, + { + execute: func() { + client.StackTraceRequest(1, 0, 20) + _ = client.ExpectStackTraceResponse(t) + + client.ScopesRequest(1000) + _ = client.ExpectScopesResponse(t) + + client.VariablesRequest(localsScope) + locals := client.ExpectVariablesResponse(t) + if locals == nil { + t.Fatal("wanted local variables, got 0") + } + + mustGetByName := func(vars []dap.Variable, names ...string) (map[string]dap.Variable, error) { + res := make(map[string]dap.Variable) + + for _, n := range names { + idx := slices.IndexFunc(vars, func(v dap.Variable) bool { + return v.Name == n + }) + + if idx == -1 { + return nil, fmt.Errorf("%s not found", n) + } + + res[n] = vars[idx] + } + + return res, nil + } + + varIdx, err := mustGetByName(locals.Body.Variables, + "jsonString", + "jsonHash", + "jsonAddr", + "bytesString", + "nonprint", + ) + if err != nil { + t.Fatal(err) + } + + longString := varIdx["jsonString"] + + if strings.Trim(varIdx["jsonAddr"].Value, `"`) != longString.MemoryReference { + t.Fatal("bad memory address") + } + + got := readVarByChunk(t, client, longString, 64) + hashed := sha256.Sum256(got.Bytes()) + hashString := hex.EncodeToString(hashed[:]) + + if strings.Trim(varIdx["jsonHash"].Value, `"`) != hashString { + t.Fatal("we got wrong values") + } + + bytes := readVarByChunk(t, client, varIdx["bytesString"], 1) + + if bytes.String() != "this\nis\nit" { + t.Fail() + } + + nonp := readVarByChunk(t, client, varIdx["nonprint"], 1) + want := []byte{242, 243, 244, 245} + + for i, b := range nonp.Bytes() { + if want[i] != b { + t.Fail() + } + } + }, + disconnect: true, + }}) + }) +} + +func readVarByChunk(t *testing.T, client *daptest.Client, v dap.Variable, chunk int) bytes.Buffer { + t.Helper() + + var got bytes.Buffer + + for off := 0; ; off += chunk { + count := chunk + client.ReadMemoryRequest(v.MemoryReference, off, count) + rm := client.ExpectReadMemoryResponse(t) + + if rm.Body.Data == "" { + break + } + + data, err := base64.StdEncoding.DecodeString(rm.Body.Data) + if err != nil { + t.Fatalf("base64 decode failed: %v", err) + } + + got.Write(data) + + if len(data) < count { + break + } + } + + return got +} + type discard struct { }