-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added functionality to track overlapping accesses #258
Conversation
I don't think this is working correctly yet. For indirect_calls/jumptable/clang:BAP, these are the relevant instructions with overlapping stack accesses:
The 128 bit access to R31 + 16 overlaps with the 64 bit access to R31 + 16 and the 64 bit access to R31 + 24. However, the analysis doesn't correctly determine that these overlap. The results it gives are that:
There is also node 450 which contains Stack_16 (of size 16) and Stack_12 (of size 4). It has three cells:
This is incorrect - accesses to R31 + 16 and R31 + 12 never overlap, and these offsets don't make any sense. Stack_12 and Stack_16 do not overlap and the analysis should not find that they do. It's alright if they both point to a collapsed node containing pointers both add_two and add_six, but it's even better if we can maintain the offsets if possible? |
4751106
to
159c5d9
Compare
This seems to be working now so I'll merge it soon. It would be nice to be able to improve the precision so it can tell that add_two is at offset 0 and add_six is at offset 8 in the combined node and then tell that 00000429_R8 is pointing to add_two, 00000438_R8 is pointing to add_six, and 0000040b_V0 is pointing to both, but that would probably be a fair bit more complex and isn't a must have. |
src/main/scala/analysis/data_structure_analysis/LocalPhase.scala
Outdated
Show resolved
Hide resolved
Oh you were planning to write tests weren't you? I'll wait for you do to those before merging. |
// 0x10DC0 contains a pointer to the procedure add_two: separate node with a single cell at the start which points to add_two once the analysis process here currently the analysis will grow the cell corresponding to 0x10DC0 the size of the access. it causes that cell to overlap the cell corresponding to 0x10DC8 and as a result their outgoing edges (their pointees) are also merged. This causes the collapse an alternative would be to not grow 0x10DC0's cell but instead do a multi load/write. |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as resolved.
This comment was marked as resolved.
Can you explain this further? I don't quite follow what the problem is, why don't we have a way of telling which offsets to use? |
after V0 := mem[0x10DC0, el]:u128 |
We do actually know the sizes. The way it works is that:
Because 0x10DC8 is 8 bytes after 0x10DC0 and they are loaded together, we know that the offset for cell 2 is 8 just from that. |
we would know the size if the pointee is a single (not merged with anything else) global object (possibly single stack objects aswell), because we can look it up in the symbol table and know its size and also know the object isn't merged with something else so we can rely on our known size. But I don't think we can tell the size in general. if pointee is not global or stack we won't be able to tell it's size, same thing follows if a stack or global object has been merged with some other nontrivial (not a place holder for SSA variables) object. But you are right we are able to get more precision in certain cases |
We know the size because there is an object at 0x10DC0 and an object at 0x10DC8, and we are accessing both via a 128-bit access to 0x10DC0. 0x10DC8 is offset from 0x10DC0 by 8 bytes, so cell2 should also have an offset of 8 bytes. This applies broadly, whether we are accessing global data, or the stack, or a heap location. We don't need to know anything about the symbol table for this (except that those objects exist at those addresses in this example). Of course, if those objects have already been merged with anything else that has caused their nodes to collapse then you can't possibly do anything except collapse these together too, but that's fine. If you don't know the sizes, you can't even know that there are overlapping accesses to begin with. |
You are so correct. OF COURSE we have sizes of the pointees because we use them to determine if there is an overlapping access. Thank you for clarifying that. My symbol table to global objects (Data Structure Nodes) conversion step is missing the middle indirection level (cell for 754 pointing to add_two) from 0x10DC0 to 754 to add_two. I'll fix that next and then should be able to get more precise overlapping accesses implemented. |
Thanks, that makes sense. I agree that we need to add that extra indirection layer that isn't in there at present. |
0185072
to
879508a
Compare
You accidentally overwrote some of your previous fixes with this force push |
879508a
to
0185072
Compare
I'm not sure if this extra indirection actually makes sense either - I think I'd gotten a bit confused when I agreed that it was needed, so my apologies for that. The issue is that 0x754 is the address of the function add_two, so the value 0x754 is a function pointer to add_two. This means that any register that contains the value 0x754 is just pointing to add_two - it isn't pointing to a pointer to add_two. That would only be the case if a register held the value 0x10DC0 (which is an address that contains the value (0x754). The difficulty we're dealing with is that we want to be able to have a node where cell 0 is add_two, and cell 8 is add_six, but I don't think that's really supported at the moment - would it be possible to implement something like that and does it make any sense as an approach? Using negative addresses doesn't really seem like a sensible way to represent this either, if we wanted indirection like that a better option would probably be a new sort of node to represent functions, rather than negative addresses which would only be confusing as they don't actually mean anything. |
I agree that the indirection doesn't seem correct specially because the global address range for the functions are shrunk but I think it makes sense to represent 0x754 as a cell pointing to add_two rather than add_two. This way we want a node where cell 0 is a pointing to add_two and cell 8 is a pointing to add_six which seems more correct than having add_two with size 20 bytes at cell 0 and add_six with size 20 bytes at cell 8. we need a better way of representing function pointers. I think it is suitable to represent them as single cell nodes with the single cell pointing to the function (e.g. add_two). However, I think here the appropriate address range is [0x754, 0x754] but we want the cell to be 8 bytes long I also agree with the negative address not being very sensible, currently all the globals have an address range but the current approach represents 0x754 to 0x754 + 8 as a pointer to add_two and hence had to put add_two itself in some range that won't be addressed. I'm still unclear on how I want to represent the function pointers tho, so I think this should be addressed after the above issue is resolved |
It depends on the semantics - varToCell maps each SSA Register to a Slice (a Cell and an offset into that Cell) - does the Cell represent the value of the register, or what the register points to? |
The cell represents the value. The cell may or may not be pointing to something |
Ah, ok, the indirection is needed then, that clears things up. At the moment it feels like it's the other way around - a DataLocation with start 0x754 seems like it is representing the memory location 0x754, not the value 0x754 (which points to said memory location), so more work may need to be done to make this clear & consistent. I think the simplest solution is that the cell with value 0x754 (which should have size 8 then) should then point to a cell containing a new type of MemoryLocation that represents a function? |
Similar work may need to be done with DataLocations that represent the addresses of global variables too - there may need to be a new MemoryLocation that represents a global variable's contents (the value that is actually at the address)? |
The value 0x754 is a function pointer and is 8 bytes long, so the cell should be 8 bytes long if it represents that as a value. What does the address range actually mean? If cells represent values, then I'm not sure the concept of an address range is needed at all? |
This pull request also seems to incorrectly identify overlapping accesses in some cases. An example of this is for correct/malloc_with_local/clang:BAP. There are no overlapping stack accesses in main(), but it incorrectly finds accesses to R31 + 4, R31 + 8 and R31 + 16 to be overlapping, even though the accesses to R31 + 4 are always 32-bit (so 4 bytes) and the other accesses are always 64-bit (so 8 bytes), meaning that there is no overlap. This is the .bir annotated with the accesses and allocations if it assists:
|
The cells represent values but only values that matter are addresses. The values don't matter themselves, all we care about is if the value is an address or not and where the address points to if it is. Therefore, I don't think we need to do this for non function globals. Same with 0x754 the value doesn't matter as much as that it is represented by a cell that points to add_two. I agree that the address range is not needed, however, it is convenient to be able to accesses globals with some unique key like their address for testing. And since the addresses are needed for finding appropriate offsets and overlapping accesses, we can represent the value 0x754 with address range 0x754 to 0x754 which is a cell that points to add_two like in the current iteration. |
You do need to change how you represent other memory locations as well because otherwise it would be treating function pointers inconsistently with other pointers. For instance, currently the size of a DataLocation is the size of the object that the cell containing it points to, but since you are changing how you represent function pointers, then the size of a DataLocation will mean the size of the object pointed to for an object pointer, but the size of the pointer itself (or just 0 currently?) for a function pointer. 'DataLocation' is also a fairly unclear name given what it actually means - it should probably be something more like 'DataPointer'. If you just want a unique key for testing, then there isn't a need for an address range at all - you could just use the value itself as a key? The value 0x754 isn't at the address 0x754, it's at the address 0x10DC0, so we need to make sure this is something that's handled consistently. |
I just realized that the overlapping accesses only work the address being read/written to is directly a stack/global but not if there is a level of indirection. If there is any indirection same issue with inter procedural overlapping access occur. This issue is solved for interprocedural stack overlapping accesses and i'm currently working on the globals. Same solution can be applied to indirect overlapping accesses. Effectively every merge (other than direct accesses to global/stack) has to be wrapped in extra functionality that checks for overlapping accesses to global/stack nodes through indirection. It's also possible to incorporate this into merge itself, but since we may require extra merges, have to find a suitable base case to end the recursion. The current solution would be somewhat inefficient. To know if a node is a reference to a particular stack, or global. We have to run find() on all the stack/globals and see if they match. Through flags we can eliminate nodes that aren't stacks or globals. Alternatively could implement a more accurate tracking of stack and globals through merges |
I think we're going to want the more accurate tracking, even if that's more work to implement? |
But if there's an easier, less efficient way, it's fine to implement that first I think? |
Yeah I'm gonna try to fix it, with the current inefficient way and then implement tracking separately |
Is this still an issue, I'm not sure if it was fixed during the other updates or if I'm missing the problem but for me R31 + 4, R31 + 8 and R31 + 16 are separate nodes with a single cell and correct accessed sizes |
I think you fixed that last week but I'll have another look to make sure now. |
Yes, that's working now. The results generally seem to make sense now from what I can see, including in the jumptable/clang test. This is the main thing that still needs to be addressed:
Currently a DataLocation can mean a location (which can even be a procedure) or a pointer to a location, and there isn't any consistency to what it means or what its size parameter means, which should be fixed. Right now the node with DataLocation(add_two's pointer, 1876, 0) (which represents a location containing the value 1876, which is a pointer to add_two) will point to the node with DataLocation(add_two, 1876, 20), which represents the actual procedure add_two with address 1876. This is inconsistent with the approach for global variables, where DataLocation(x, 69680, 4) represents a location containing the value 69680 (which is a pointer to x, not the actual location at address 69680), and points to another cell which represents the actual contents of x. |
function pointers are handled similar to other globals now. The only difference is that the first cell will point to a special function Node instead of empty/placeholder node as its content. |
I'm happy to merge this unless if you find other problems with it. I found an issue with the cloning of stack cells discussed in #274. I will address that in a general refactoring of how the stack handling works (remove SAA, track stack offsets via cells themselves, negative/overflow issues and cloning of stack cells) |
I still think it would be good to change 'DataLocation' to 'DataPointer' as it represents a pointer, not a location.
Thanks, this makes sense. I'll merge this soon once I run some final checks. It's fine to leave the stack overhaul to another pull request. |
I guess I'm still a bit confused by why the globalMappings table works like it does, why do we need to have a concept of an AddressRange when the globalMappings table is a mapping from AddressRanges to Fields, which are node + offset pairs that represent a cell containing the value that is the base of the address range? It's just a bit convoluted as-is. I feel like either it would make more sense for globalMappings to map to the cells representing the contents of the address range, or just have a mapping from addresses to the cell that contains that value? |
scripts/lift.sh
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. I think I may have just changed permissions on this file but I haven't changed any of its content
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strange!
def multiAccess(lhsOrValue: Cell, pointer: Cell, size: Int): Unit = { | ||
// TODO there should be another check here to see we cover the bytesize | ||
// otherwise can fall back on expanding | ||
|
||
val diff = pointer.offset - lhsOrValue.offset | ||
val startPointerOffset = pointer.offset | ||
val lhsNode = lhsOrValue.node.get | ||
|
||
|
||
val pointers = pointer.node.get.cells.filter((offset, _) => offset >= startPointerOffset && offset < startPointerOffset + size).toSeq.sortBy((offset, cell) => offset) | ||
for (((offset, cell), i) <- pointers.zipWithIndex) { | ||
val lhs = lhsNode.addCell(offset - diff, 0) // todo check if 0 is the right size | ||
graph.find(cell).growSize(if i < pointers.size - 1 then (pointers(i + 1)._1 - offset - diff).toInt else (size - (offset - diff)).toInt) | ||
val res = graph.mergeCells(lhs, graph.adjust(graph.find(cell).getPointee)) | ||
graph.handleOverlapping(res) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method could really use a comment explaining what it is doing, as it is very convoluted as-is
The field is because we could have a global begin in the middle of a cell. We could instead have a cell/internal offset of the cell. It's just easier to get the node and call getCell(offset)/addCell(offset) on it rather than get a cell and then calculate the total offset by adding the cell's offset to the internal The address ranges map to the base of the range. Having the end allows us to know whether an address in the middle of the range is pointing to the same memory location but at an offset. We will not be able to determine this otherwise (for example from size of nodes/cells since those sizes are also affected by other merges) The address range in effect stores the size of the actual "global" which starts at the node + offset pair we are pointing to |
Like if we have a global variable x with address 69680 and size 4, then we will get the following entry in globalMappings:
Here, the cell at offset 0 in Node 558 is a cell that contains a pointer to the global variable x, not the contents of the global variable x, so why don't we just map directly from AddressRange(69680, 69684) to the cell that represents the contents of global variable x? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all fine except 'DataLocation' should be renamed as discussed
This is true for the local graphs where the range always has a value at the beginning of the first cell in the node but after a bunch of merges in the subsequent graphs the address range may start in the middle of a cell. So even if we have mapping like This is an alternative way we could track it but it's easier to track the offset directly instead of having to get the cell's offset and add it to it's internal offset to create "auxiliary" cells So instead we have |
This isn't what I'm suggesting, and seem to be largely equivalent to what we already have. What I'm saying is that that Cell is one that contains a pointer to the global variable x - it doesn't represent the contents of global variable x. I'm wondering why we don't directly map from the AddressRange to the Cell (or Slice, or Field, whichever works) that represents the contents of the memory location represented by that AddressRange? |
Sorry I misunderstood. I think we need a pointer to the location. That way if a global address is stored in the heap/stack or another global we can treat it like any other pointer. we could load a global pointer through indirection and not worry about it being a global (we kind of have to check because of overlapping accesses currently but we can treat them like any other dereference, otherwise we would have to merge the global location with its content but merge the pointees of any other access) |
I think we need to revert this for the time being, the unsoundness issues are more significant than I thought. |
I've reverted this, so make a new pull request for points_to_analysis_overlapping_access I will outline the issues I've discovered in #276 now that I have time to do so. |
This pull request address the issue of overlapping accesses for DSA as discussed by issue #254
Remaining: