debugger icon indicating copy to clipboard operation
debugger copied to clipboard

Consider discarding the memory cache only when the target stops, and not when the target resumes

Open xusheng6 opened this issue 1 year ago • 1 comments

Right now we discard the memory cache as soon as the target resume execution. This is technically right, since once the target is running, all the memory content we are seeing are no longer up-to-date. However, this also causes certain problems, especially when the target will be running for a while (e.g., waiting for some console input) and it does not stop again immediately. This can cause several issues:

  1. Since the backing memory has gone, the analysis can change in subtle and hard-to-perceive ways. One user reported the strings are gone (i.e., those constant strings are no longer shown), and the underlying cause is those bytes are no longer readable when the target is running. Worth still, if the user is brave enough to update the analysis, the entire function will become a question mark, again, because the memory bytes are gone and the analysis cannot do anything.
  2. If I wish to add a breakpoint somewhere while the target is running, it is also very tricky, since if I navigate to another function, chances are its analysis will be updated automatically, and the function becomes a big question mark, and there is no way to add a breakpoint. The only workaround is to pause the target, add the breakpoint, and then resume it. Which is a bit counterproductive

In light all of this, I suggest we should only keep the memory cache while the target is running, and only discard it when the target actually stops and becomes accessible again.

xusheng6 avatar Jun 28 '24 04:06 xusheng6

Also -- for consistency, should we also keep everything else, e.g., registers, stack traces, etc, while the target is running?

xusheng6 avatar Jun 28 '24 04:06 xusheng6

I experimented this and the original proposal alone does not solve the problem. Think of the following case:

  1. The user launches the target
  2. The user steps the target once
  3. The user resumes the target, which then it runs on its own
  4. The user navigates to another function
  5. The memory bytes of the function is unavailable, and become question marks

Note the situation in the last step happens because the memory bytes cache for the function is already invalidated in the 2nd step. In some cases, the re-analysis (if any) can cause the read of the memory bytes, thus updating the cache. However, that is NOT reliable, and it only happens by chance.

In light of this, I think we should enhance our handling of the memory bytes. When we invalidate the cache, we should do a soft delete -- only marking the bytes as outdated but do not immediately discard the value. Now, if the user wishes to read such outdated byte value, one of the two happens:

  1. If the target is stopped, then read the value from the backend and update the cache
  2. If the target is running, or otherwise inaccessible, return the last known value for it

When the target exits, we should always do a hard invalidation which removes all such cache values

xusheng6 avatar Aug 09 '24 07:08 xusheng6

Another related scenario:

  1. The user launches the target
  2. The user places a hardware read breakpoint
  3. The breakpoint hits
  4. Binja navigates to the address, but create a new function at it

This is a more complex scenario and it has been puzzling me for a while. This is because, at a certain point after the target has resumed, and before the target stops at the hardware breakpoint, we discarded the cache of the memory bytes. Now, for some reason, the function containing the instruction that triggers the hardware breakpoint gets updated (e.g., due to being referenced by some other changes), and due to the lack of the memory bytes, the analysis will NOT be able to find the original basic blocks in its original state. The analysis will create a dumb function which only contains one (invalid) instruction.

Now, when the breakpoint hits, binja will try to find whether there is a function that contains the current instruction pointer value. And since the function it belongs to no longer contains the correct basic blocks, binja will think no functions contain this address, and proceed to create a new one at it

xusheng6 avatar Aug 09 '24 07:08 xusheng6