• The Old New Thing

    How do I enumerate drives the same way that the NET USE command does?

    • 14 Comments

    If you use the Remote Desktop Connection client to connect to another computer, you have the option of making your local drives available to the remote computer.

    A customer wanted to know how to enumerate all the drives on the local machine. The were able to get the volumes mapped to drive letters, but they also wanted to get the redirected drives injected by Terminal Services. (Mind you, these aren't volumes that are assigned drive letters, so it's not clear why they are interested in them, but whatever.)

    With the NET USE command, they see the Terminal Services volumes in Explorer, and they can be browsed via \\tsclient\d:

    Status       Local     Remote                    Network
    
    -------------------------------------------------------------------------------
                           \\TSCLIENT\D              Microsoft Terminal Services
    The command completed successfully.
    

    The customer wanted to enumerate these Terminal Services client volumes. "How does the NET USE command enumerate these volumes?"

    Let's write that program. Remember, Little Programs do little to no error checking.

    #define UNICODE
    #define _UNICODE
    #include <windows.h>
    #include <winnetwk.h>
    #include <stdio.h>
    
    void report(PCWSTR pszLabel, PCWSTR pszValue)
    {
     printf("%ls = %ls\n", pszLabel, pszValue ? pszValue : L"(null)");
    }
    
    int __cdecl main(int, char **)
    {
     HANDLE hEnum;
     WNetOpenEnum(RESOURCE_CONNECTED,
                  RESOURCETYPE_DISK,
                  0,
                  NULL,
                  &hEnum);
    
     DWORD cbBuffer = 65536;
     void *buffer = LocalAlloc(LMEM_FIXED, cbBuffer);
     LPNETRESOURCE pnr = (LPNETRESOURCE)buffer;
    
     DWORD err;
     do {
      DWORD cEntries = INFINITE;
      DWORD cb = cbBuffer;
      err = WNetEnumResource(hEnum, &cEntries, buffer, &cb);
      if (err == NO_ERROR || err == ERROR_MORE_DATA) {
       for (DWORD i = 0; i < cEntries; i++) {
        report(L"localName", pnr[i].lpLocalName);
        report(L"remoteName", pnr[i].lpRemoteName);
        report(L"provider", pnr[i].lpProvider);
        printf("\n");
       }
      }
     } while (err == ERROR_MORE_DATA);
    
     LocalFree(buffer);
     WNetCloseEnum(hEnum);
     return 0;
    }
    

    We open an enumeration for connected disks and then start enumerating out of it. The usage pattern for WNet­Enum­Resources is kind of messy, with a bunch of in/out parameters that need to get reset each time. Each time, we say "Enumerate as much as you can into this 64KB buffer" and then print what we got. If we were told, "There's still more," then we go back and ask for more.

    That's all. Nothing particularly fancy.

  • The Old New Thing

    How to view the stack of a user-mode thread when its kernel stack has been paged out

    • 4 Comments

    Suppose you have a machine that has crashed, and your investigation shows that the reason is that there is a critical section that everybody is waiting for. While waiting for that critical section, work piles up, and eventually the machine keels over. Suppose further that this crash is given to you in the form of a kernel debugger.

    In case it wasn't obvious, by "you" I mean "me".

    Okay, so the critical section that is the cause of the logjam is this one:

    1: kd> !cs CONTOSO!g_csDataLock
    -----------------------------------------
    Critical section   = 0x00007ff7f0ed2f68 (CONTOSO!g_csDataLock+0x0)
    DebugInfo          = 0x0000000022f2efd0
    LOCKED
    LockCount          = 0x5D
    WaiterWoken        = No
    OwningThread       = 0x0000000000004228
    RecursionCount     = 0x1
    LockSemaphore      = 0x17A0
    SpinCount          = 0x00000000020007cb
    

    "Great," you say. "I just need to look at thread 0x4228 to see why it is stuck.

    1: kd> !process -1 4
    PROCESS ffffe000047ae900
        SessionId: 1  Cid: 0604    Peb: 7ff74ecfa000  ParentCid: 05cc
        DirBase: 0eb07000  ObjectTable: ffffc000014c5680  HandleCount: 7003.
        Image: contoso.exe
    
            ...
            THREAD ffffe0000c136080  Cid 0604.4228  Teb: 00007ff74e94c000 Win32Thread: fffff90144edea60 WAIT
            ...
    

    Woo-hoo, there's the thread. Now I just need to switch to its context to see what it is stuck on.

    1: kd> .thread ffffe0000c136080
    Can't retrieve thread context, Win32 error 0n30
    

    Okay, that didn't work out too well. Now what?

    Even though the kernel stack is paged out, the user-mode stack may still be available.

    1: kd> !thread ffffe0000c136080
    THREAD ffffe0000c136080  Cid 0604.4228  Teb: 00007ff74e94c000
           Win32Thread: fffff90144edea60 WAIT: (UserRequest) UserMode Non-Alertable
        ffffe000077a7830  NotificationEvent
    Not impersonating
    DeviceMap                 ffffc00000e89c80
    Owning Process            ffffe000047ae900       Image:         contoso.exe
    Attached Process          N/A            Image:         N/A
    Wait Start TickCount      12735890       Ticks: 328715 (0:01:25:36.171)
    Context Switch Count      75             IdealProcessor: 2
    UserTime                  00:00:00.000
    KernelTime                00:00:00.031
    Kernel stack not resident.
    

    The limits of the user-mode stack are kept in the Teb.

    2: kd> !teb 00007ff74e94c000
    TEB at 00007ff74e94c000
        ExceptionList:        0000000000000000
        StackBase:            00000001027d0000
        StackLimit:           00000001027c2000
        SubSystemTib:         0000000000000000
        FiberData:            0000000000001e00
        ArbitraryUserPointer: 0000000000000000
        Self:                 00007ff74e94c000
        EnvironmentPointer:   0000000000000000
        ClientId:             0000000000000604 . 0000000000004228
        RpcHandle:            0000000000000000
        Tls Storage:          000000010132dfc0
        PEB Address:          00007ff74ecfa000
        LastErrorValue:       1008
        LastStatusValue:      c0000034
        Count Owned Locks:    2
        HardErrorMode:        0
    

    We now use the trick we learned some time ago where we grovel the stack of a thread without knowing where its stack pointer is.

    In this case, the groveling is made easier because we already know that everybody is waiting on the data lock. The data lock is taken in only two functions, so it's a matter ot looking for any occurrences of one of those two functions. And here it is: Data­Wrapper::Verify­Data.

    00000001`027cef88  00007ffe`f1bbac9a NTDLL!NtWaitForSingleObject+0xa
    00000001`027cef90  00000000`00006ac8
    00000001`027cef98  00007ffe`ef1bd085 KERNELBASE!WaitForSingleObjectEx+0xa5
    00000001`027cefa0  00000000`00006ac8
    00000001`027cefa8  00000000`00000000
    00000001`027cefb0  00000000`026d0000
    00000001`027cefb8  00000000`00000000
    00000001`027cefc0  00000001`01739ee0
    00000001`027cefc8  00000000`00000001
    00000001`027cefd0  00000000`00000048
    00000001`027cefd8  00000001`00000001
    00000001`027cefe0  00000000`00000000
    00000001`027cefe8  00000000`00000000
    00000001`027ceff0  00000000`00000000
    00000001`027ceff8  00000000`00000000
    00000001`027cf000  00000000`00000000
    00000001`027cf008  00000000`00000000
    00000001`027cf010  00000000`00000000
    00000001`027cf018  00007ff7`eefe6540 FABRIKAM!Lock::IsInitialized+0xfc
    00000001`027cf020  00000000`00000000
    00000001`027cf028  00000000`3b803fa0
    00000001`027cf030  00000000`00006ac8
    00000001`027cf038  00007ff7`eeff3c43 FABRIKAM!AccessRequest::WaitTimeout+0x87
    00000001`027cf040  00000000`ffffffff
    00000001`027cf048  10f513ec`6a161fb8
    00000001`027cf050  00000000`00000000
    00000001`027cf058  00000000`00006ac8
    00000001`027cf060  00000000`3b803fb8
    00000001`027cf068  00007ff7`eeff3cc0 FABRIKAM!AccessRequest::Wait+0x18
    00000001`027cf070  00000000`8007000e
    00000001`027cf078  00000000`fd416ff0
    00000001`027cf080  4fe80c4a`51236583
    00000001`027cf088  00000000`3b803fb8
    00000001`027cf090  00000000`ffffffff
    00000001`027cf098  00000000`00000000
    00000001`027cf0a0  00000000`3b803fa0
    00000001`027cf0a8  00007ff7`da8375b7 FABRIKAM!DataAccess::RequestAccess+0x93
    00000001`027cf0b0  00000000`3b803fa0
    00000001`027cf0b8  00000000`3b803fb8
    00000001`027cf0c0  4fe80c4a`51236583
    00000001`027cf0c8  10f513ec`6a161fb8
    00000001`027cf0d0  00000000`00000076
    00000001`027cf0d8  00007ff7`f07a8619 CONTOSO!Widget::SetColor+0x9
    00000001`027cf0e0  000095dc`90897985
    00000001`027cf0e8  00000000`00000110
    00000001`027cf0f0  00000000`00000000
    00000001`027cf0f8  00000000`00000000
    00000001`027cf100  00007ff7`4dd8000c
    00000001`027cf108  00007ff7`f07a85e5 CONTOSO!Widget::UpdateColor+0x39
    00000001`027cf110  00000000`ffc03f90
    00000001`027cf118  00000001`027cf1e8
    00000001`027cf120  00000000`ffc03fc8
    00000001`027cf128  00000000`00000000
    00000001`027cf130  00000000`00000000
    00000001`027cf138  00007ff7`4dd8000c
    00000001`027cf140  00007ff7`da5bc420 FABRIKAM!DataAccess::`vftable'+0x18
    00000001`027cf148  00000000`ffc03f90
    00000001`027cf150  00000001`027cf260
    00000001`027cf158  00007ff7`f0b3459c CONTOSO!DataWrapper::VerifyData+0x428
    00000001`027cf160  00000000`3b803fa0
    00000001`027cf168  00007ff7`f0ed1d30 CONTOSO!g_DataManager
    00000001`027cf170  00000000`fe196f10
    00000001`027cf188  00000000`00000001
    00000001`027cf190  00000000`00000000
    00000001`027cf198  00000001`027cf270
    00000001`027cf1a0  00000001`027cf480
    00000001`027cf1a8  00007ff7`00000001
    00000001`027cf1b0  00000001`027cf220 
    00000001`027cf1b8  00000000`00000000 
    00000001`027cf1c0  00000000`00000000 
    

    I left the red herrings in place just to make things a little more interesting.

    The Data­Wrapper::Verify­Data method enters the critical section and then calls Data­Access::Request­Access via a virtual method call:

    00007ff7`f0b3458f mov     dword ptr [rsp+28h],eax
    00007ff7`f0b34593 mov     eax,dword ptr [rsi+10h]
    00007ff7`f0b34596 mov     dword ptr [rsp+20h],eax
    00007ff7`f0b3459a call    qword ptr [rdi] ←
    

    Let's disassemble the start of Data­Access::Request­Access to see how it sets up its stack. This will help us interpret the other values in the stack dump.

    0: kd> u 00007ff7`da8375b7-93
    FABRIKAM!DataAccess::RequestAccess
    ;; function prologue
    00007ff7`da837524 mov     rax,rsp
    00007ff7`da837527 mov     qword ptr [rax+8],rbx
    00007ff7`da83752b mov     qword ptr [rax+20h],r9
    00007ff7`da83752f mov     qword ptr [rax+18h],r8
    00007ff7`da837533 mov     qword ptr [rax+10h],rdx
    00007ff7`da837537 push    rbp
    00007ff7`da837538 push    rsi
    00007ff7`da837539 push    rdi
    00007ff7`da83753a push    r12
    00007ff7`da83753c push    r13
    00007ff7`da83753e push    r14
    00007ff7`da837540 push    r15
    00007ff7`da837542 sub     rsp,70h
    ;; function body
    00007ff7`da837546 lea     rsi,[rcx+18h]
    00007ff7`da83754a mov     rdi,rcx
    00007ff7`da83754d mov     rbp,r9
    00007ff7`da837550 mov     rcx,rsi
    00007ff7`da837553 call    qword ptr [FABRIKAM!_imp_EnterCriticalSection]
    00007ff7`da837559 xor     r13b,r13b
    00007ff7`da83755c mov     ebx,8007000Eh
    ...
    00007ffe`da8375b1 call    FABRIKAM!AccessRequest::Wait
    

    We can replay the above code in our head and annotate the stack trace accordingly. On entry to the function, the stack pointer is 00000001`027cf158 (the return address). The function stashes some registers in the caller-provided spill area and it pushes others onto the stack, and then it subtracts some space for local variables as well as for outbound parameters of functions it intends to call.

    /-00000001`027cf0b0  00000000`3b803fa0
    | 00000001`027cf0b8  00000000`3b803fb8
    | 00000001`027cf0c0  4fe80c4a`51236583
    | 00000001`027cf0c8  10f513ec`6a161fb8
    | 00000001`027cf0d0  00000000`00000076
    | 00000001`027cf0d8  00007ff7`f07a8619 CONTOSO!Widget::SetColor+0x9
    | 00000001`027cf0e0  000095dc`90897985
    | 00000001`027cf0e8  00000000`00000110
    | 00000001`027cf0f0  00000000`00000000
    | 00000001`027cf0f8  00000000`00000000
    | 00000001`027cf100  00007ff7`4dd8000c
    | 00000001`027cf108  00007ff7`f07a85e5 CONTOSO!Widget::UpdateColor+0x39
    | 00000001`027cf110  00000000`ffc03f90
    \-00000001`027cf118  00000001`027cf1e8
      00000001`027cf120  00000000`ffc03fc8 // VerifyData's r15
      00000001`027cf128  00000000`00000000 // VerifyData's r14
      00000001`027cf130  00000000`00000000 // VerifyData's r13
      00000001`027cf138  00007ff7`4dd8000c // VerifyData's r12
      00000001`027cf140  00007ff7`da5bc420 FABRIKAM!DataAccess::`vftable'+0x18 // VerifyData's rdi
      00000001`027cf148  00000000`ffc03f90 // VerifyData's rsi
      00000001`027cf150  00000001`027cf260 // VerifyData's rbp
      00000001`027cf158  00007ff7`f0b3459c CONTOSO!DataWrapper::VerifyData+0x428 ← ESP is here
      00000001`027cf160  00000000`3b803fa0 // VerifyData's rbx
      00000001`027cf168  00007ff7`f0ed1d30 CONTOSO!g_DataManager // VerifyData's rdx
      00000001`027cf170  00000000`fe196f10 // VerifyData's r8
      00000001`027cf188  00000000`00000001 // VerifyData's r9
      00000001`027cf190  00000000`00000000
      00000001`027cf198  00000001`027cf270
      00000001`027cf1a0  00000001`027cf480
      00000001`027cf1a8  00007ff7`00000001
      00000001`027cf1b0  00000001`027cf220 
      00000001`027cf1b8  00000000`00000000 
      00000001`027cf1c0  00000000`00000000 
    

    The region marked in brackets is the 0x70 bytes of space for local variables and outbound parameters. Notice that some red herring function pointers are in that space. Those are probably variables that haven't been initialized yet, and the memory happened previously to have been used to hold some return addresses.

    A reassuring observation is that the rdx coming from Verify­Data is the address of CONTOSO!g_Data­Manager. That is the second function parameter (or first, if you aren't counting the hidden this) to Request­Access.

    Another reassuring observation is that that Verify­Data's rdi points into the vtable for Data­Access, since that matches the code we saw at the call point: call qword ptr [rdi].

    The mov rdi, rcx instruction in the function body tells us that the function stashed its this pointer in rdi. That's good info to keep track of, because that will let us look at the Data­Access object once we figure out what is in rdi.

    The next function on the stack is Access­Request::Wait.

    FABRIKAM!AccessRequest::Wait:
    00007ff7`eeff3ca8 sub     rsp,38h
    00007ffe`eeff3cb3 mov     dword ptr [rsp+20h],0FFFFFFFFh
    00007ffe`eeff3cbb call    FABRIKAM!AccessRequest::WaitTimeout
    00007ffe`eeff3cc0 add     rsp,38h
    00007ffe`eeff3cc4 ret
    

    This function doesn't bother saving any registers; it just reserves space for local variables and outbound parameters. From inspection, you can see that this is a simple wrapper that passes all its parameters onward to Wait­Timeout, with an INFINITE tacked onto the end, so this function has no local variables at all. Everything is just for outbound parameters.

    We can annotate some more entries in our stack trace.

    00000001`027cf070  00000000`8007000e // spill space for WaitTimeout
    00000001`027cf078  00000000`fd416ff0 // spill space for WaitTimeout
    00000001`027cf080  4fe80c4a`51236583 // spill space for WaitTimeout
    00000001`027cf088  00000000`3b803fb8 // spill space for WaitTimeout
    00000001`027cf090  00000000`ffffffff // INFINITE parameter
    00000001`027cf098  00000000`00000000 // unused
    00000001`027cf0a0  00000000`3b803fa0 // unused
    00000001`027cf0a8  00007ff7`da8375b7 FABRIKAM!DataAccess::RequestAccess+0x93
    

    The next function on the list is Access­Request::Wait­Timeout.

    FABRIKAM!AccessRequest::WaitTimeout:
    00007ff7`eeff3bbc mov     qword ptr [rsp+8],rbx
    00007ff7`eeff3bc1 mov     qword ptr [rsp+10h],rbp
    00007ff7`eeff3bc6 push    rsi
    00007ff7`eeff3bc7 sub     rsp,20h
    00007ff7`eeff3bcb mov     ebx,edx
    00007ff7`eeff3bcd mov     rsi,rcx
    00007ff7`eeff3bd0 mov     edx,0Bh
    00007ff7`eeff3bd5 mov     rcx,r8
    

    This function stashes two registers in the parameter spill space, pushes one onto the stack, and reserves another 0x20 bytes for local use (outbound parameters).

    00000001`027cf040  00000000`ffffffff // spill space for WaitForSingleObjectEx
    00000001`027cf048  10f513ec`6a161fb8 // spill space for WaitForSingleObjectEx
    00000001`027cf050  00000000`00000000 // spill space for WaitForSingleObjectEx
    00000001`027cf058  00000000`00006ac8 // spill space for WaitForSingleObjectEx
    00000001`027cf060  00000000`3b803fb8 // Wait's rsi
    00000001`027cf068  00007ff7`eeff3cc0 FABRIKAM!AccessRequest::Wait+0x18
    00000001`027cf070  00000000`8007000e // Wait's rbx
    00000001`027cf078  00000000`fd416ff0
    00000001`027cf080  4fe80c4a`51236583
    00000001`027cf088  00000000`3b803fb8
    

    Notice that the stashed rbx value is 8007000E, which conveniently lines up with the mov ebx,8007000Eh instruction in Data­Access::Request­Access. That's a bit reassuring, since it's another sign that we're on the right track.

    Next up is Wait­For­Single­Object­Ex.

    0: kd> u 00007ffe`ef1bd085 -a5
    KERNELBASE!WaitForSingleObjectEx
    00007ffe`ef1bcfe0 mov     r11,rsp
    00007ffe`ef1bcfe3 mov     qword ptr [r11+8],rbx
    00007ffe`ef1bcfe7 mov     dword ptr [r11+18h],r8d
    00007ffe`ef1bcfeb push    rsi
    00007ffe`ef1bcfec push    rdi
    00007ffe`ef1bcfed push    r14
    00007ffe`ef1bcfef sub     rsp,80h
    00007ffe`ef1bcff6 mov     ebx,r8d
    

    Incorporating this prologue into our stack annotation yields

    /-00000001`027cefa0  00000000`00006ac8 // spill space for NtWaitForSingleObject
    | 00000001`027cefa8  00000000`00000000 // spill space for NtWaitForSingleObject
    | 00000001`027cefb0  00000000`026d0000 // spill space for NtWaitForSingleObject
    | 00000001`027cefb8  00000000`00000000 // spill space for NtWaitForSingleObject
    | 00000001`027cefc0  00000001`01739ee0
    | 00000001`027cefc8  00000000`00000001
    | 00000001`027cefd0  00000000`00000048
    | 00000001`027cefd8  00000001`00000001
    | 00000001`027cefe0  00000000`00000000
    | 00000001`027cefe8  00000000`00000000
    | 00000001`027ceff0  00000000`00000000
    | 00000001`027ceff8  00000000`00000000
    | 00000001`027cf000  00000000`00000000
    | 00000001`027cf008  00000000`00000000
    | 00000001`027cf010  00000000`00000000
    \-00000001`027cf018  00007ff7`eefe6540 FABRIKAM!Lock::IsInitialized+0xfc
      00000001`027cf020  00000000`00000000 // WaitTimeout's r14
      00000001`027cf028  00000000`3b803fa0 // WaitTimeout's rdi
      00000001`027cf030  00000000`00006ac8 // WaitTimeout's rsi
      00000001`027cf038  00007ff7`eeff3c43 FABRIKAM!AccessRequest::WaitTimeout+0x87
      00000001`027cf040  00000000`ffffffff // WaitTimeout's rbx
      00000001`027cf048  10f513ec`6a161fb8
      00000001`027cf050  00000000`00000000 // WaitTimeout's r8
      00000001`027cf058  00000000`00006ac8
    

    Ooh, another red herring function pointer got caught in the local variables.

    Putting everything together results in the following annotated stack, with red herrings removed.

    00000001`027cef88  00007ffe`f1bbac9a NTDLL!NtWaitForSingleObject+0xa
    00000001`027cef90  00000000`00006ac8
    00000001`027cef98  00007ffe`ef1bd085 KERNELBASE!WaitForSingleObjectEx+0xa5
    00000001`027cefa0  00000000`00006ac8
    00000001`027cefa8  00000000`00000000
    00000001`027cefb0  00000000`026d0000
    00000001`027cefb8  00000000`00000000
    00000001`027cefc0  00000001`01739ee0
    00000001`027cefc8  00000000`00000001
    00000001`027cefd0  00000000`00000048
    00000001`027cefd8  00000001`00000001
    00000001`027cefe0  00000000`00000000
    00000001`027cefe8  00000000`00000000
    00000001`027ceff0  00000000`00000000
    00000001`027ceff8  00000000`00000000
    00000001`027cf000  00000000`00000000
    00000001`027cf008  00000000`00000000
    00000001`027cf010  00000000`00000000
    00000001`027cf018  00007ff7`eefe6540
    00000001`027cf020  00000000`00000000 // WaitTimeout's r14
    00000001`027cf028  00000000`3b803fa0 // WaitTimeout's rdi
    00000001`027cf030  00000000`00006ac8 // WaitTimeout's rsi
    00000001`027cf038  00007ff7`eeff3c43 FABRIKAM!AccessRequest::WaitTimeout+0x87
    00000001`027cf040  00000000`ffffffff // WaitTimeout's rbx
    00000001`027cf048  10f513ec`6a161fb8
    00000001`027cf050  00000000`00000000 // WaitTimeout's r8
    00000001`027cf058  00000000`00006ac8
    00000001`027cf060  00000000`3b803fb8 // Wait's rsi
    00000001`027cf068  00007ff7`eeff3cc0 FABRIKAM!AccessRequest::Wait+0x18
    00000001`027cf070  00000000`8007000e // Wait's rbx
    00000001`027cf078  00000000`fd416ff0
    00000001`027cf080  4fe80c4a`51236583
    00000001`027cf088  00000000`3b803fb8
    00000001`027cf090  00000000`ffffffff // INFINITE parameter
    00000001`027cf098  00000000`00000000
    00000001`027cf0a0  00000000`3b803fa0
    00000001`027cf0a8  00007ff7`da8375b7 FABRIKAM!DataAccess::RequestAccess+0x93
    00000001`027cf0b0  00000000`3b803fa0
    00000001`027cf0b8  00000000`3b803fb8
    00000001`027cf0c0  4fe80c4a`51236583
    00000001`027cf0c8  10f513ec`6a161fb8
    00000001`027cf0d0  00000000`00000076
    00000001`027cf0d8  00007ff7`f07a8619
    00000001`027cf0e0  000095dc`90897985
    00000001`027cf0e8  00000000`00000110
    00000001`027cf0f0  00000000`00000000
    00000001`027cf0f8  00000000`00000000
    00000001`027cf100  00007ff7`4dd8000c
    00000001`027cf108  00007ff7`f07a85e5
    00000001`027cf110  00000000`ffc03f90
    00000001`027cf118  00000001`027cf1e8
    00000001`027cf120  00000000`ffc03fc8 // VerifyData's r15
    00000001`027cf128  00000000`00000000 // VerifyData's r14
    00000001`027cf130  00000000`00000000 // VerifyData's r13
    00000001`027cf138  00007ff7`4dd8000c // VerifyData's r12
    00000001`027cf140  00007ff7`da5bc420 FABRIKAM!DataAccess::`vftable'+0x18 // VerifyData's rdi
    00000001`027cf148  00000000`ffc03f90 // VerifyData's rsi
    00000001`027cf150  00000001`027cf260 // VerifyData's rbp
    00000001`027cf158  00007ff7`f0b3459c CONTOSO!DataWrapper::VerifyData+0x428
    00000001`027cf160  00000000`3b803fa0 // VerifyData's rbx
    00000001`027cf168  00007ff7`f0ed1d30 CONTOSO!g_DataManager // VerifyData's rdx
    00000001`027cf170  00000000`fe196f10 // VerifyData's r8
    00000001`027cf188  00000000`00000001 // VerifyData's r9
    00000001`027cf190  00000000`00000000
    00000001`027cf198  00000001`027cf270
    00000001`027cf1a0  00000001`027cf480
    00000001`027cf1a8  00007ff7`00000001
    00000001`027cf1b0  00000001`027cf220 
    00000001`027cf1b8  00000000`00000000 
    00000001`027cf1c0  00000000`00000000 
    

    From this, we can also suck out the this pointer passed to Data­Access::Request­Access. We saw that it was stashed in rdi. The Wait function doesn't use rdi (because if it did, it would have saved the old value), so its rdi is the same as Request­Access's rdi. Similarly, the Wait­Timeout function does not use rdi. Therefore, when Wait­For­Single­Object saves the rdi register, it is saving the value from Data­Access::Request­Access.

    00000001`027cf028  00000000`3b803fa0 // WaitTimeout DataAccess's rdi
    

    And that is the this pointer that lets us study the Data­Access object to figure out why its access request is not completing.

  • The Old New Thing

    When does GetTickCount consider the system to have started?

    • 45 Comments

    The Get­Tick­Count and Get­Tick­Count­64 functions return "the number of milliseconds that have elapsed since the system was started." (The 32-bit version wraps around after around 50 days.) But when exactly is the system considered to have started? Is it when power is applied to the computer? When the BIOS completes POST? When the user picks the operating system from the boot menu? When the kernel switches to protected mode?

    It isn't defined exactly when the timer starts. Because that's not its purpose.

    The purpose of Get­Tick­Count is to let you measure intervals of time. It provides a common clock source so that multiple components can coordinate their actions. It also allows you to retrieve the tick count at one point, then retrieve the tick count at another point, subtract them, and conclude how much time has elapsed between those two points. The absolute value of the tick count is not meaningful. The only way to extract meaning from it is to subtract it from another tick count to get the delta.

    In fact, on debugging builds of Windows, the kernel artificially sets the Get­Tick­Count counter to "one hour before 32-bit timer tick rollover"; it effectively backdates the boot time by around 50 days. This is done to help identify bugs related to timer tick rollover.

    If your goal is to measure operating system boot time from the application of power to the computer, then Get­Tick­Count is not going to be useful. After all, Windows isn't even running at the moment you apply power to the computer. The BIOS does its work without any operating system all, so Windows has no idea how long the BIOS took to POST. The text in MSDN could be a bit more explicit and say "elapsed since Windows started", or it could be pointlessly nitpicky and say "elapsed since the Windows HAL initialized the programmable interval timer."

    Better would be if it simply described how the timer is intended to be used. "Get­Tick­Count returns a value which increases at a rate of 1000 per second." Perhaps with some clarifying text: "By convention, the zero point of the Get­Tick­Count counter is the approximate time the system booted. Note, however, that this convention is violated on occasion (such as on a checked build of Windows), so applications should not ascribe any meaning to the zero point of the tick counter."

    If you want to know how much time elapsed since the application of power, you need to use a stopwatch.

  • The Old New Thing

    How can I wait until all startup applications have finished launching?

    • 27 Comments

    A customer wanted to know how to detect that all startup applications have finished launching. They wanted to wait until everything settled down before proceeding with some task.

    What if two programs did this?

    Suppose two programs both wanted to detect that all startup applications have finished launching. Each one would sit there waiting for the other, because this is one of those I want to be the last to do something, even laster than the other guy who wants to do something last things.

    If you want to wait until the system is idle to perform some task, you can use the Task Scheduler to create an idle-time task. This has the benefit of coordinating multiple idle-time tasks so that you don't have the phenomenon of two applications both trying to do things when they think nobody else is doing anything, resulting in an Alphonse-and-Gaston state: The system goes idle, and both Alphonse and Gaston start doing their idle-time tasks. But Alphonse notices that Gaston is busy and backs off; at the same time, Gaston notices that Alphonse is busy and backs off. Now the system is idle again, and both Alphonse and Gaston start their idle-time tasks, and the cycle continues. The computer gets into this weird state where it keeps waking up, going to sleep, waking up, going to sleep, and never actually making progress.

  • The Old New Thing

    A little cheat in my Tiger Beat photo homage

    • 9 Comments

    One thing nobody has called out in my tribute to the Bill Gates Tiger Beat photo, either because it was too subtle or too obvious, is that the photo is actually a mirror image.

    The arrangement of furniture in the room was not correct: The big table was on the wrong side of the room. It was also too heavy to move around, so we cheated. We staged the entire picture as a mirror image, flipping the Windows screen shot. And then back in the virtual darkroom, Ariel flipped the photo to put the furniture on the correct side of the photo.

    Here are the clues in the photo:

    • The SONY logo on the monitor.
    • The Multiscan G500 and Trinitron branding on the monitor.
    • The Microsoft logos on the binder on the table.
    • The arrows on the recycle bin propping open the door.

    Chatter: The day after I put this article into the queue (which makes it visible to Microsoft employees), somebody posted a comment pointing it out. Coincidence? You decide.

  • The Old New Thing

    Kicking around a function that formats stuff

    • 17 Comments

    Today's "Little Program" is really a "Little Puzzle" that got out of hand.

    This started out as a practical question: This code fragment screams out for some sort of simplification. (I've changed the names of the classes.)

    class FrogProperty
    {
     public string Name { get; private set; }
     public string Value { get; private set; }
     ...
    }
    
    class ToadProperty
    {
     public string Name { get; private set; }
     public string Value { get; private set; }
     ...
    }
    
    var frogStuff = new List<string>();
    foreach (var frogProp in FrogProperties) {
      frogStuff.Add(string.Format("{0}: {1}", frogProp.Name, frogProp.Value));
    }
    frogStuff.Sort();
    Munge(frogStuff);
    
    var toadStuff = new List<string>();
    foreach (var toadProp in ToadProperties) {
      toadStuff.Add(string.Format("{0} = {1}", toadProp.Name, toadProp.Value));
    }
    toadStuff.Sort();
    Munge(toadStuff);
    
    var catStuff = new List<string>();
    foreach (var cat in Cats) {
      catStuff.Add(string.Format("{0}", cat.Name));
    }
    catStuff.Sort();
    Munge(catStuff);
    
    var dogStuff = new List<string>();
    foreach (var dogProp in DogProperties) {
      dogStuff.Add(string.Format("{0} {1}", dogProp.Name, dogProp.Value));
    }
    dogStuff.Sort();
    Munge(dogStuff);
    
    ...
    

    Clearly, the pattern is

    var stuff = new List<string>();
    foreach (var thing in thingCollection) {
     stuff.Add(string.Format(formatstring, thing.Name, [optional: thing.Value]));
    }
    stuff.Sort();
    Munge(stuff);
    

    Everything here is pretty straightforward, except for the string.Format part. Can we write a function that takes a thing and formats it in a somewhat flexible manner?

    Let's start with the Name-and-Value cases. We might try something like this:

    public static string FormatNameValue<T>(this T t, string format)
    {
     return string.Format(format, t.Name, t.Value);
    }
    

    But then we'd run into trouble, because there is no constraint on T, so the compiler will complain, "I don't know how to get a Name or a Value from an object."

    And since Frog­Property and Toad­Property do not have a common base class, you're kind of stuck.

    One way out would be to use the new dynamic type:

    public static string FormatNameValue<T>(this T t, string format)
    {
     dynamic d = t;
     return string.Format(format, d.Name, d.Value);
    }
    

    But that won't work in the Name-only case:

    cat.FormatNameValue("{0}");
    

    The cat object has a Name but no Value. The attempt to read the Value will raise an exception (even though it is never consumed by the format).

    Maybe we can turn to reflection.

    public static string FormatNameValue<T>(this T t, string format)
    {
     return string.Format(format,
                          typeof(T).GetProperty("Name").GetValue(t, null),
                          typeof(T).GetProperty("Value").GetValue(t, null));
    }
    

    This still raises an exception if there is no Value, but we can detect the missing Value before we run into trouble with it.

    static object GetPropertyOrNull<T>(this T t, string prop)
    {
     var propInfo = typeof(T).GetProperty(prop);
     return propInfo == null ? null : propInfo.GetValue(t, null);
    }
    
    public static string FormatNameValue<T>(this T t, string format)
    {
     return string.Format(format,
                          t.GetPropertyOrNull("Name"),
                          t.GetPropertyOrNull("Value"));
    }
    

    Okay, now we're getting somewhere.

    But before getting to deep into this exercise, I should point out that another way to solve this problem is to turn it inside-out. Instead of making the munger understand all of the different objects, why not make each object understand munging?

    class FrogProperty : IFormattable
    {
     public string Name { get; private set; }
     public string Value { get; private set; }
     public override ToString(string format, IFormatProvider formatProvider)
     {
      switch (format) {
      case "Munge":
       return string.Format(formatProvider,"{0}: {1}", Name, Value);
      default:
       return ToString(); // use object.ToString();
      }
     }
    }
    
    class Cat : IFormattable
    {
     public string Name { get; private set; }
     public override ToString(string format, IFormatProvider formatProvider)
     {
      switch (format) {
      case "Munge":
       return string.Format(formatProvider,"{0}", Name);
      default:
       return ToString(); // use object.ToString();
      }
     }
    }
    

    The generic helper function would then be

    var stuff = new List<string>();
    foreach (var thing in thingCollection) {
     stuff.Add(string.Format("{0:Munge}", thing);
    }
    stuff.Sort();
    Munge(stuff);
    

    Okay, fine, rain on my little puzzle parade.

    Let's ignore this very useful advice and proceed ahead with our puzzle, because we're determined to see how far we can go, even if it's in the wrong direction.

    Now that we have Format­Name­Value, we might say, "What about generalizing to cases where we want properties other than Name and Value?" One design would be to pass in a format string and list of properties you want to fill in:

    thing.FormatProperties("{0}: {1} (modified by {2})",
                           "Name", "Value", "ModifiedBy");
    

    Our Format­Name­Value function would go something like this:

    public static string FormatProperties<T>(
        this T t, string format, params string[] props)
    {
     object[] values = new object[props.Length];
     for (var i = 0; i < props.Length; i++) {
      values[i] = typeof(T).GetProperty(props[i]).GetValue(t, null);
     }
     return string.Format(format, values);
    }
    

    This suffers from a problem common to most formatters: Once you get more than a few insertions, it becomes hard to figure out which one matches up to what. So I'm going to try something radical:

    static Regex identifier = new Regex(@"(?<={)(.*?)(?=[:}])");
    
    // pedants would use
    //identifier = new RegEx(@"[_\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}]" +
    //       @"[_\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\d\p{Pc}\p{Mn}\p{Mc}]");
    
    public static string FormatProperties<T>(this T t, string format)
    {
      var values = new ArrayList();
      int count = 0;
      format = identifier.Replace(format, (m) => {
        values.Add(typeof(T).GetProperty(m.Value).GetValue(t, null));
        return (count++).ToString();
      });
      return string.Format(format, values.ToArray());
    }
    

    Instead of separating the properties from the format, I embed them in the format.

    thing.FormatProperties("{Name}: {Value} (modified by {ModifiedBy})");
    

    Note that I explicitly exclude colons from identifiers. That lets me do things like this:

    var result =
      (new System.IO.FileInfo(@"C:\Windows\Explorer.exe"))
        .FormatProperties("Created on {CreationTime:F} " +
                          "{Length} bytes in size");
    

    The property names are extracted and replaced with corresponding numbers, but the format string remains, allowing it to be used to alter the final formatting of the property.

    Okay, at this point I figured I had gone far enough. The fun had run out, so I decided to stop.

  • The Old New Thing

    Why does an attempt to create a SysLink control in my plug-in sometimes fail?

    • 1 Comments

    A customer had written a plug-in for some application, and they found that their plug-in was unable to create a SysLink control via the Create­Window­ExW function. The same code in a standalone application works fine, but when the code is placed in their plug-in, the code fails.

    Debugging showed that the call to Init­Common­Controls­Ex succeeded, but the Create­Window­ExW call failed with "Cannot find window class."

    The customer is another victim of not keeping their eye on the activation context.

    They attached a manifest to their DLL so that the call to Init­Common­Controls­Ex maps to the version of the common controls library that supports the SysLink control. But they did nothing to ensure that that context was active at the time they called Create­Window­ExW.

    The customer's plug-in clearly falls into the case Adding Visual Style Support to an Extension, Plug-in, MMC Snap-in or a DLL That Is Brought into a Process. but they failed to follow the instructions provided therein (which boil down to "use isolation awareness").

    From the symptoms, it appears that the host application for their plug-in does not activate a version-6 common controls manifest at the time it calls into the plug-in, which means that your attempt to create version-6 common controls will fail.

    On the other hand, the standalone application probably uses the technique given in Using ComCtl32.dll Version 6 in an Application That Uses Only Standard Extensions, which activates the version-6 common controls when the process starts and leaves it active for the duration of the process.

  • The Old New Thing

    Raymond, why did you stop studying Chinese? It has no grammar!

    • 68 Comments

    One of my colleagues, a native Chinese speaker, asked me whether I was still learning Mandarin Chinese. I told him that I had given up. He was baffled by this.

    "But Chinese is such a simple language. It has no grammar!"

    Now of course, Mandarin has a grammar, because every language has a grammar.

    This is one of the curses of being a native speaker of a language: You don't even realize how hard your language is. As far as you're concerned, your native language is as easy as falling off a log.¹

    Now, it's true that Mandarin has almost no inflections, unlike most European languages. But that's not the same as saying it has no grammar. It's just that the grammar moves from being internal (inflection) to external (helping words and word order).

    Sidebar: David from Popehat lays out some of the simplifications, but I think he oversimplifies the use of the completion marker 了. It's not strictly speaking a past-tense marker, at least not in the sense we consider it in English. Proper use of 了 is more complicated, and this page tries to explain some of the subtleties. David later tries to explain Mandarin phonology. I must admit that I have an advantage in already having a tonal language wired into my brain, so I don't have the hurdle of learning to hear and speak tones. I just have to learn to hear and speak different tones. Which is still frustrating. End sidebar

    One of the consequences of "your own native language is simple" is that native speakers are sometimes the worst choices for explaining their own language, since they simply fail to recognize how weird their language is. An example I gave some time ago was that elusive third tone. If you ask a native speaker how it is pronounced, they will say one thing ("dipping tone"), but then when they themselves speak the third tone they do something completely different ("low level tone"). Native speakers are so convinced that the third tone dips that when you call them on it, they insist that the tone dipped, when in fact it barely moved at all.

    That conversation with my father went something like this.

    Me: "In that sentence, you said ⟨low level tone⟩."

    My father: "No, I didn't. I said ⟨dipping tone⟩."

    Me: "Well, sure, that time you said ⟨dipping tone⟩. But in the original sentence, you said ⟨low level tone⟩."

    My father: "No, I didn't. Listen again. ⟨repeats sentence and uses low level tone⟩."

    Me: "There, see? You used ⟨low level tone⟩."

    My father: "No, I didn't. Here, you repeat back to me what you think I said."

    Me: "⟨says sentence with low level tone⟩."

    My father: "There, you got it!"

    Me: "But I used the wrong tone! I should have said, ⟨says sentence with dipping tone⟩."

    My father: "No, that's wrong. You exaggerated the tone too much."

    That last remark from my father was what made it click for me: The low level tone and the dipping tone are complementary allophones. (My father, of course, has no idea what a complementary allophone is, but that's okay.)

    Another example of native speakers not seeing the complexity in their own language is the use of the negative adverb 没. Mandarin has two main adverbs that mean "not": 不 and 没. If you ask a native speaker, they will tell you, "It's very simple. 不 is the general-purpose negation, and 没 is used only to negate the verb to have. In other words, 没 is always followed by 有." But then you will see that native speakers use 没 to negate all sorts of things that aren't 有. If you point this out, they will retcon it by saying that the phrase 没關係 ("no connection", which is an idiom that means "it doesn't matter, don't worry about it") is really a shorthand for 没有關係 ("doesn't have a connection"). Native speakers play this card whenever an out-of-place 没 shows up. "Oh, it's negating an invisible 有." If you ask them how to tell when there is an invisible 有 in a sentence, they will say "You just have to know," or sometimes the circular "Stick a 没 in front and see if it makes sense."

    Sidebar: Here's a page that tries to explain the difference between 不 and 没. The way I internalize it based on limited observation is to say that 不 is not tied to a moment in time (innate or habitual property), whereas 没 refers to a particular incident (momentary property).

    不濕
    doesn't get wet
    (it's water-resistant)
    没濕
    isn't wet
    (he has an umbrella)
    不飲牛奶
    doesn't drink milk
    (he's lactose intolerant)
    没飲牛奶
    isn't drinking milk
    (he chose water)

    This is similar to the distinction between English simple ("I do") and progressive ("I am doing"). Furthermore, 没 carries a sense of "yet"; you are denying that something is true now, but expecting that to change in the future. End sidebar

    One downside about having such a superficially simple grammar is that it makes the language much more ambiguous. The more complex grammar of European languages acts as a checksum. If I say, "He are coming," then you know that something went wrong. The grammatical doodads act like signposts to confirm that you the listener are parsing the sentence correctly. It's like the road sign after every highway exit that reassures you, "You are still on Highway 405 Northbound." One of my colleagues told me that he missed those signs on his trip to Italy. There would be signs labeling each exit, but rarely was there a sign telling you what highway you were currently on!

    To me, Chinese is difficult to learn because of its lack of guideposts that help steer you onto the right track. Without them, many sentences end up ambiguous. (In that example, the lack of any grammatical particle that distinguishes imperative from declarative mood led to the confusion.) The relative scarcity of grammatical particles makes me feel like I'm talking baby-talk. "Me want eat cookie."²

    Resolving ambiguity is made even harder by the fact that every word in Mandarin has about a dozen homophones (fortunately, most of them not used in everyday speech), so you aren't even sure what word you're dealing with at the moment you hear it. You just know it's one of these two or three, and you have to wait and see which one actually makes sense when combined with the other words in the sentence (some of which may themselves also be ambiguous).

    Adding to the ambiguity is that in many cases, you can omit words from a sentence if they are implied from context. So you now have to juggle the ambiguous mapping of sounds to words, the ambiguous grammatical context of those words (was that a statement or a direct order?), and choosing which implied words to insert to support your conclusion! Of course, native speakers can resolve all of these ambiguities very quickly, having been doing so since birth, and they are much better at picking up other cues (such as where the speaker speeds up and slows down) to help steer toward the correct interpretation. Indeed, in the language I learned as a young child, I can resolve these ambiguities with no difficulty at all.

    Sidebar: Even native speakers sometimes have to go into explicit ambiguity-resolution mode by adding clarifying context. This happens in English occasionally: You might say, "He had a bat (the animal)" because the shorter sentence "He had a bat" would be ambiguous. Did he have an animal or an instrument for striking? End sidebar

    One thing I do like to quibble about is the treatment of classifiers in Mandarin. Most people treat them as a quirk of the language, making them sound like an oddball feature that doesn't exist in European languages. An analogue in English would be the word "pair" when applied to scissors or pants. You can't say "a scissors" or "a pants"; you have to say "a pair of scissors" or "a pair of pants." (Particularly confusing because a "pair" of scissors or pants is still one article.) In Mandarin, every noun has a corresponding classifier.

    You can think of classifiers as the Mandarin version of grammatical gender. The nouns in the language fall into around 170 different categories, and you just have to know which category word goes with each noun. There are patterns that help the learning process, but there are always exceptions that you simply must memorize.

    For example, 条 is generally used for long, thin, flexible things, like a fish or a ribbon. But you also use it for dogs. Oh, and also for skirts and dresses. Go figure.

    So the next time a native Mandarin speaker complains that English has all these arbitrary rules that serve no purpose other than making the language harder to learn, just ask them about classifiers. (They will naturally defend classifiers by saying that they are completely obvious and in no way arbitrary.)

    Anyway, the bit about classifiers explains why the subway ticket vending machine asks you how many "sheets" you want: In Mandarin, it is very common to omit the noun and use only the classifier when the noun is implied from context. This happens in English, too. If you are a shop that repairs scissors, the clerk might ask, "What's wrong with this pair?" as shorthand for "What's wrong with this pair of scissors?"

    The classifier word for ticket is 張 which translates as sheet. The full question is "How many sheets of tickets?" But since you are at a ticket vending machine, the noun is implied from context, and the shorter sentence "How many sheets?" is used instead.

    ¹ This natural tendency to think of what you do as normal reveals itself in the words that the Chinese language uses to refer to itself. The name for the country of China is 中國, which translates as the middle kingdom, because by an amazing coincidence, China happens to be right in the middle of the map. And the name for the language itself is 普通話, which translates as normal speech, because we all talk normally; it's the foreigners who talk funny by using their own words for everything.³

    ² In practice, the distinction between baby-talk and adult-talk in Chinese is accomplished in two ways. First, babies have a specialized vocabulary: babies say doggy instead of dog, for example. Second, adults employ modal particles which convey the attitude of the speaker. Cantonese is notorious for having a large number of these sorts of particles. I don't know most of them, so my speech tends to come off as rather rude and abrupt.

    ³ Someone said that a neighbor of his grandmother complained, "I don't understand why people in foreign countries bother to learn a second language. Why don't they just talk normal?"

  • The Old New Thing

    How do I get a handle to the primary monitor?

    • 21 Comments

    There are various ways of getting a monitor. You can get the monitor from a point, from a rectangle, or from a window. But how do you get the primary monitor?

    The primary monitor is defined to be the one which has (0, 0) as its origin. Therefore, one solution is

    HMONITOR GetPrimaryMonitor()
    {
     POINT ptZero = { 0, 0 };
     return MonitorFromPoint(ptZero,
                             MONITOR_DEFAULTTOPRIMARY);
    }
    

    The desktop window by convention is deemed to reside primarily on the primary monitor, so you could also use this:

    HMONITOR GetPrimaryMonitor()
    {
     return MonitorFromWindow(GetDesktopWindow(),
                              MONITOR_DEFAULTTOPRIMARY);
    }
    

    Or you could just pass the null window handle. This is technically an illegal parameter, but by specifying MONITOR_DEFAULT­TO­PRIMARY, you are saying, "If anything goes wrong, give me the primary monitor."

    HMONITOR GetPrimaryMonitor()
    {
     return MonitorFromWindow(nullptr,
                              MONITOR_DEFAULTTOPRIMARY);
    }
    

    In this case, we are intentionally going astray because we want to kick in the MONITOR_DEFAULT­TO­PRIMARY behavior.

  • The Old New Thing

    Passing the incorrect object type for a handle, how bad is it?

    • 24 Comments

    A customer asked a somewhat strange question: "We are calling Set­Event but passing the handle to a waitable timer. Application Verifier reports, "Incorrect object type for handle." But the code works well. We want to know the risks of passing the wrong object type to Set­Event. Is the recommendation only to pass handles of type "Event" to Set­Event?

    Let's answer those questions in reverse order.

    Yes, the recommendation is only to pass handles of type "Event" to Set­Event, just as the recommendation is only to pass handles of type "Semaphore" to Release­Semaphore, and more generally, only to pass valid parameters to functions.

    What is the risk of passing the wrong object type? You're lucky that the kernel does object type validation before proceeding, so your error is caught during parameter validation and the function fails with the error ERROR_INVALID_HANDLE (or status code STATUS_OBJECT_TYPE_MISMATCH, if the function returns status codes instead of error codes).

    Of course, if you are encountering this problem only because you are using a handle after closing it (and then the handle got recycled as a timer handle), then you merely got lucky. Maybe tomorrow you won't be so lucky, and the handle will get recycled as another unrelated event. Tomorrow, your Set­Event call will succeed and set some other guy's event. This will probably cause that other guy to get really confused. "This event is set when the modulator has finished calibrating. But the event is getting signaled before the calibration is complete, so my code ends up using an uncalibrated modulator! I set a breakpoint on my Set­Event call, and it never fires, yet the event is set. Help me debug this. I've spent a week trying to figure out what's wrong!"

    As to the final remark, "But the code works well," it's not clear what the customer meant by that. What does "works well" mean in this context? Do they mean, "The event is successfully set even though it's not an event"? How can you successfully perform an event operation on something that isn't an event? Or perhaps they mean, "Our code seems to work okay in spite of this mistake." The operative phrase there is "seems to". It may seem to work well, but someday it won't, and at the most inconvenient time.

Page 4 of 439 (4,382 items) «23456»