Monitor Module (FsMinifilterMonitor)

The user-mode monitor acts as a bridge between the kernel driver and the scanner. It receives file system event messages from the minifilter via a filter communication port and forwards scan-worthy events to the scanner via a named pipe.

Related: Communication Architecture ยท Kernel Driver Module ยท Scanner Module ยท Kernel โ†” User Interface


1. Source Files

File Purpose
FsMinifilterMonitor/main.cpp Single-file application: port connection, message loop, deduplication, pipe forwarding

2. Module Architecture

flowchart TB
    subgraph Monitor["FsMinifilterMonitor.exe"]
        direction TB
        
        subgraph Init["Initialization"]
            Connect["FilterConnectCommunicationPort()\nโ†’ port handle"]
        end
        
        subgraph Loop["Main Message Loop"]
            GetMsg["FilterGetMessage()\n(blocking)"]
            ExtFilter["IsExecutableOrDll()"]
            Dedup["ShouldSendScanRequest()\n(dedup cache)"]
            BuildReq["Build SCAN_REQUEST"]
            SendPipe["SubmitScanRequestToScanner()\n(named pipe)"]
        end
        
        subgraph PipeMgmt["Pipe Management"]
            Ensure["EnsurePipeConnection()\n(lazy + retry)"]
            Write["WriteFile(pipe, SCAN_REQUEST)"]
            Recover["Reset handle on failure"]
        end

        Connect --> GetMsg
        GetMsg --> ExtFilter --> Dedup --> BuildReq --> SendPipe
        SendPipe --> Ensure --> Write
        Write -.-> Recover
    end

    style Init fill:#3a0ca3,color:#fff
    style Loop fill:#4361ee,color:#fff
    style PipeMgmt fill:#7209b7,color:#fff

3. Main Loop

flowchart TD
    Start["wmain()"]
    Start --> ConnPort["FilterConnectCommunicationPort(\n  '\\FsMinifilterPort')"]
    
    ConnPort --> Failed{"FAILED(hr)?"}
    Failed -->|Yes| Exit1["Print error, exit(1)"]
    Failed -->|No| MsgLoop["Enter message loop"]
    
    MsgLoop --> GetMsg["FilterGetMessage(\n  port, &message)"]
    
    GetMsg --> Succeeded{"SUCCEEDED(hr)?"}
    Succeeded -->|No| LostConn{"ERROR_INVALID_HANDLE?"}
    LostConn -->|Yes| Exit2["Connection lost, exit"]
    LostConn -->|No| MsgLoop
    
    Succeeded -->|Yes| IsExe{"IsExecutableOrDll(\n  message.FilePath)?"}
    IsExe -->|No| MsgLoop
    
    IsExe -->|Yes| ShouldSend{"ShouldSendScanRequest(\n  message.FilePath)?"}
    ShouldSend -->|No| MsgLoop
    
    ShouldSend -->|Yes| Build["Build SCAN_REQUEST:\n  filePath = message.FilePath\n  pid = message.ProcessId\n  timestamp = now"]
    
    Build --> Submit["SubmitScanRequestToScanner(&req)"]
    Submit --> MsgLoop

    style Exit1 fill:#e63946,color:#fff
    style Exit2 fill:#e63946,color:#fff
    style Build fill:#2d6a4f,color:#fff

4. Key Functions

4.1 IsExecutableOrDll

BOOL IsExecutableOrDll(const WCHAR* filePath)

Quick case-insensitive extension check using _wcsicmp on the last 4 characters. Returns TRUE for .exe and .dll.

Note: This duplicates the kernel-side IsTargetExtension() check. Both are needed because the kernel sends all message types (including DELETE which applies to all files), and the monitor adds a second layer of filtering.

4.2 ShouldSendScanRequest (Deduplication)

BOOL ShouldSendScanRequest(const WCHAR* filePath)
flowchart TD
    Input["filePath"]
    Input --> Now["now = GetTickCount64()"]
    
    Now --> Cleanup{"now - lastCleanup\n> 30,000ms?"}
    Cleanup -->|Yes| Evict["Evict entries > 5s old\nfrom cache"]
    Cleanup -->|No| Lookup
    Evict --> Lookup
    
    Lookup --> Found{"filePath in cache\nAND age < 5,000ms?"}
    Found -->|Yes| RetFalse["Return FALSE\n(duplicate)"]
    Found -->|No| Update["cache[filePath] = now"]
    Update --> RetTrue["Return TRUE\n(send it)"]

    style RetFalse fill:#6c757d,color:#fff
    style RetTrue fill:#2d6a4f,color:#fff

Data structure: std::unordered_map<std::wstring, ULONGLONG> mapping file paths to the last-seen timestamp.

Parameter Value
Cooldown window 5,000ms
Cache cleanup interval 30,000ms
Cleanup strategy Iterate all entries, erase those older than cooldown

4.3 EnsurePipeConnection

BOOL EnsurePipeConnection()

Lazy connection with retry logic:

  • If g_hPipe != INVALID_HANDLE_VALUE: return TRUE (already connected)
  • Otherwise: retry CreateFile(PIPE_NAME) up to 20 times with 100ms sleep between attempts
  • Returns FALSE if all retries fail

4.4 SubmitScanRequestToScanner

BOOL SubmitScanRequestToScanner(const SCAN_REQUEST* req)
  1. Call EnsurePipeConnection() to ensure pipe is ready
  2. WriteFile(g_hPipe, req, sizeof(*req))
  3. On write failure: close handle, set g_hPipe = INVALID_HANDLE_VALUE for reconnection on next call

5. Message Transformation

The monitor transforms kernel messages into scan requests:

flowchart LR
    subgraph KernelMsg["FILTER_MESSAGE (from kernel)"]
        FMH["FILTER_MESSAGE_HEADER"]
        MM["MINIFILTER_MESSAGE {\n  MessageType: ULONG\n  ProcessId: ULONG\n  FilePath: WCHAR[520]\n}"]
    end

    subgraph ScanReq["SCAN_REQUEST (to scanner)"]
        SR["SCAN_REQUEST {\n  filePath: WCHAR[260]\n  pid: DWORD\n  timestamp: FILETIME\n}"]
    end

    KernelMsg -->|"Extract + transform"| ScanReq
    
    style KernelMsg fill:#4361ee,color:#fff
    style ScanReq fill:#2d6a4f,color:#fff

Transformation steps:

  1. filePath โ† message.Message.FilePath (truncated from 520 to 260 chars)
  2. pid โ† message.Message.ProcessId
  3. timestamp โ† GetSystemTimeAsFileTime() (added by monitor, not kernel)

6. Error Handling

Scenario Behavior
Cannot connect to filter port Print error, exit with code 1
FilterGetMessage returns ERROR_INVALID_HANDLE Driver disconnected, exit message loop
FilterGetMessage returns other error Retry (continue loop)
Cannot connect to scanner pipe Print warning, skip scan request
Pipe write fails Close handle, reconnect on next request

Next Steps