Add ObjC.ProxyBlock.
Use stackblock instead of globalblock to solve the strange crash encountered in the process of replacing block implementation.
Adjust blockDescriptorAllocSize and blockDescriptorOffsets to fit ProxyBlock's 3part-Descriptor, and keep offset.rest for Original ObjC.block.
Change descriptor->size from blockDescriptorDeclaredSize to blockSize.
Problems
Haven't written a test case yet.
If the script is disposed, the application may crash.
(ObjC.block have the same problem?)
Reason
blockDescriptorDeclaredSize and blockSize
When Block_copy is used on stackblcok, descriptor->size is used to request memory to store "another part".So size should be block's size.(But have no effect on GlobalBlock)
//Code from libclosure-74/runtime.cpp _Block_copy
// Its a stack block. Make a copy.
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
ProxyBlock
The globleblock has some difficulties in solving life cycle problems. For example:
-[NSURLSession dataTaskWithURL:completionHandler:], send a network request and process the response in a callback. Now you want to know which request created the response.
Solution 1 Try the example provided in the documentation:
const pendingBlocks = new Set();
Interceptor.attach(..., {
onEnter(args) {
const block = new ObjC.Block(args[4]);
pendingBlocks.add(block); // Keep it alive
const appCallback = block.implementation;
block.implementation = (error, value) => {
// Do your logging here
const result = appCallback(error, value);
pendingBlocks.delete(block);
return result;
};
}
});
It looks good, but what happens if each incoming block is the same? For example, a developer is only concerned about whether a network request is in error and receives a response with a globalblock.
Solution 2
Create a new block to replace the old one. And call the old one in the new block. Work like ObjC.Proxy.
handler = _Block_copy(handler);
const ori_block = new ObjC.Block(handler);
const new_block = new ObjC.Block({
implementation: function (data, resp, error) {
// Do your logging here
const result = ori_block.implementation(data, resp, error);
_Block_release(ori_block.handle);
setTimeout(function () { pendingBlocks.delete(K); }, 400);//When?
return result;
},
types: ori_block.types,
});
pendingBlocks.add(new_block);
return new_block;
It looks good, doesn't it? Each request received a different callback. Just release the callback at the right time. No, not that simple.
What is the difference between pendingBlocks.delete in the first two examples? Why need 'setTimeout'?
The problem is that the callback "holder" does not necessarily release the callback immediately after the callback has been executed, and we don't know when the release will occur by current implementation.(Unless we monitor the call of _Block_relase and judge whether this parameter is a callback generated by frida, ah,and we have to maintain retain count ourselves)
For most cases, setTimeout400 is enough, but some applications will crash unless setTimeout is set to more than ten seconds. One possibility I can think of is that the callback will be released when an autorelease pool is released, so there are differences between different applications.
In summary, it might be a good idea to use a stackblock and wait for the lifecycle call(copy/dispose helper) of the OC.
Thanks! (And so sorry for the delay here, I somehow missed this PR.) This sounds great to me. This needs test coverage before we can land it though. Let me know if you need any guidance there.
Change
Add ObjC.ProxyBlock. Use stackblock instead of globalblock to solve the strange crash encountered in the process of replacing block implementation.
Adjust blockDescriptorAllocSize and blockDescriptorOffsets to fit ProxyBlock's 3part-Descriptor, and keep offset.rest for Original ObjC.block.
Change descriptor->size from blockDescriptorDeclaredSize to blockSize.
Problems
Haven't written a test case yet.
If the script is disposed, the application may crash. (ObjC.block have the same problem?)
Reason
blockDescriptorDeclaredSize and blockSize
When Block_copy is used on stackblcok, descriptor->size is used to request memory to store "another part".So size should be block's size.(But have no effect on GlobalBlock)
ProxyBlock
The globleblock has some difficulties in solving life cycle problems. For example: -[NSURLSession dataTaskWithURL:completionHandler:], send a network request and process the response in a callback. Now you want to know which request created the response.
Solution 1 Try the example provided in the documentation:
It looks good, but what happens if each incoming block is the same? For example, a developer is only concerned about whether a network request is in error and receives a response with a globalblock.
Solution 2 Create a new block to replace the old one. And call the old one in the new block. Work like ObjC.Proxy.
It looks good, doesn't it? Each request received a different callback. Just release the callback at the right time. No, not that simple.
What is the difference between pendingBlocks.delete in the first two examples? Why need 'setTimeout'?
The problem is that the callback "holder" does not necessarily release the callback immediately after the callback has been executed, and we don't know when the release will occur by current implementation.(Unless we monitor the call of _Block_relase and judge whether this parameter is a callback generated by frida, ah,and we have to maintain retain count ourselves)
For most cases, setTimeout400 is enough, but some applications will crash unless setTimeout is set to more than ten seconds. One possibility I can think of is that the callback will be released when an autorelease pool is released, so there are differences between different applications.
In summary, it might be a good idea to use a stackblock and wait for the lifecycle call(copy/dispose helper) of the OC.