"target/client": We use "client" to mean the tool itself which I assume you do not mean: I assume you mean the application. The client is assumed to be trusted, and there are no mechanisms to monitor everything it does if it chooses to not go through DR interfaces (e.g., raw syscalls to change page protections).
Self-modifying code anywhere else but an ntdll routine hooked by DR works fine, but unfortunately that dispatcher has to be hooked by DR. DR has to hide its hook from the app: see
https://github.com/DynamoRIO/dynamorio/blob/master/core/arch/interp.c#L3296 which I assume is why the change is not visible. When that was developed, -handle_ntdll_modify was set to DR_MODIFY_FAIL or DR_MODIFY_NOP. Only later was it set to DR_MODIFY_ALLOW but I don't think support was ever added for the app changing DR's hooks, either to preserve DR's functionality or to faithfully execute the app. I would suggest filing an issue and submitting a pull request to add that support (otherwise it is not clear whether anyone else would have time to do it). It might be tricky to do without races while the page is writable, but some kind of mechanism for re-patching on the page protection restore would be the simplest; handling a lack of restore and faults in the meantime are also tricky. Maybe it's better to keep the page read-only and emulate the writes. Just talking out loud here. If this were a syscall hook, a workaround would be to remove DR's patch which is not absolutely required; but the exception dispatcher is required.