jacereda / fsatrace

Filesystem access tracer
ISC License
81 stars 12 forks source link

Tracing multiple subprocesses doesn't work on Windows #23

Closed ndmitchell closed 5 years ago

ndmitchell commented 5 years ago

If I create a file twice.bat with contents:

cat foo.txt
cat bar.txt

Then it only records the first call to cat, not the second. Similarly, if I do gcc -c main.c then it traces the call to cc1 (which loads main.c and writes the .s file), but not the call to as (which writes main.o).

The problem (as best I can tell...) is that patchInstalled asks if this thread has previously installed a patch. But, if this thread previously installed a patch for a different process, it returns True even though the patch isn't valid. I "fixed" this by changing the value stored in the Tls to be the process of the thread passed to NtResumeThread. I have no real idea what I'm doing, but it fixes the problem, and tracing it seems to approximately work. Any advice?

ndmitchell commented 5 years ago

Concretely:

diff --git a/src/win/hooks.c b/src/win/hooks.c
index eb1e66a..529612b 100644
--- a/src/win/hooks.c
+++ b/src/win/hooks.c
@@ -3,6 +3,8 @@
 #endif
 #include <winternl.h>
 #include <limits.h>
+#include <processthreadsapi.h>
+

 #undef ASSERT
 #include "dbg.h"
@@ -213,7 +215,7 @@ static NTSTATUS NTAPI hNtQueryFullAttributesFile(POBJECT_ATTRIBUTES oa, PFILE_NE
 static NTSTATUS NTAPI hNtResumeThread(HANDLE th, PULONG sc) {
    NTSTATUS r;
    D;
-   if (!patchInstalled())
+   if (!patchInstalled(GetProcessIdOfThread(th)))
        injectThread(th);
    r = oNtResumeThread(th, sc);
    return r;
diff --git a/src/win/patch.c b/src/win/patch.c
index 6fdc26d..7b63f0a 100644
--- a/src/win/patch.c
+++ b/src/win/patch.c
@@ -95,12 +95,12 @@ void patchInstall(void *orig, void *hook, void **preal, const char *nm) {
    dbg("modules patched\n");
 }

-int patchInstalled() {
+int patchInstalled(DWORD old) {
    int ret;
    ASSERT(s_hooked);
-   ret = (int)(intptr_t)TlsGetValue(s_hooked);
-   CHK(TlsSetValue(s_hooked, (void *)1));
-   return ret;
+   ret = (DWORD)(intptr_t)TlsGetValue(s_hooked);
+   CHK(TlsSetValue(s_hooked, (void *)old));
+   return ret == old;
 }

 void patchInit() {
diff --git a/src/win/patch.h b/src/win/patch.h
index a9628ec..64ba3d0 100644
--- a/src/win/patch.h
+++ b/src/win/patch.h
@@ -1,5 +1,4 @@
 void patchInit();
 void patchTerm();
 void patchInstall(void *orig, void *hook, void **preal, const char *nm);
-int patchInstalled();
-
+int patchInstalled(DWORD old);
jacereda commented 5 years ago

Looks certainly better than what we have now, but I guess it isn't bulletproof either. A different thread could call ResumeThread() and we could end up with 2 patches installed. I guess a 4 KB bit mask indexed by the lower bits of (pid/4) could be quite robust.

jacereda commented 5 years ago

Here's what I thought could be a solution, but it didn't pass the tests.

https://github.com/jacereda/fsatrace/commit/7abb16b4d4b14c1e575184b267faf19632e253ce

Unfortunately I can't check with a windows box yet.

ndmitchell commented 5 years ago

I'll try tomorrow on a real windows box. Why do the pid byte masking and not just have an actual array with the values seen?

jacereda commented 5 years ago

OK, I think I found the bug. Please, check if the fix-23 branch works for you.

jacereda commented 5 years ago

Yeah, it's kind of silly to worry about that table size considering the amount of memory I reserve for the logs :)

jacereda commented 5 years ago

Oh, you mean doing a lookup in the array? Yes, that would probably be a better option. Feel free to change it.

ndmitchell commented 5 years ago

It worked for me, thanks a lot. Given it's currently working I'd probably just leave it at that.