Jirka Hanika
Jirka Hanika

Reputation: 13539

Call to `Environment.GetEnvironmentVariable` affects subsequent calls

In the following code sample executed on .NET 4.5.1 or .NET 4.5.2 (same results), something strange happens when the code queries a "nonexistent" variable. Another, perfectly existent variable "myvar" whose value is an empty string, will stop being seen through a call to GetEnvironmentVariable, but it is still seen via iteration of the whole environment.

This behavior probably cannot be recreated purely using .NET APIs because they don't allow setting an environment variable to an empty string; but the native APIs allow that.

It feels very strange that a call to Environment.GetEnvironmentVariable would make another variable disappear, or half-disappear, from the environment.

The behavior with target framework set to .NET 2.0 is subtly different. The mismatch between getting myvar directly, and iteration, occurs immediately after the native call to SetEnvironmentVariable - it is not necessary to query for another variable to see it.

Edit: Adding Charset=CharSet.Auto, as kindly suggested by Hans Passant (thanks!) reduces the craziness level of .NET 4.5.x to that of .NET 2.0, exactly as described in the previous paragraph, apart from fixing Unicode handling. The DllImport is also missing a SetLastError parameter, but this is just an artificial example and we know that this native call is successful on the Win32 level. So, so far no explanation. I know of several ways around the problem, but I'd like to better understand what I'm seeing.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("Kernel32.dll")]
        public static extern int SetEnvironmentVariable(string name, string value);

        static void Main(string[] args)
        {
            ShowMyVar();

            Environment.SetEnvironmentVariable("myvar", "somevalue");
            ShowMyVar();

            Environment.SetEnvironmentVariable("myvar", String.Empty);
            ShowMyVar();

            SetEnvironmentVariable("myvar", String.Empty);
            ShowMyVar();
            // once again, for good measure.
            ShowMyVar();

            Console.WriteLine("\nOkay, sane results so far.  Now let's query an unrelated non-existent variable.");

            Environment.GetEnvironmentVariable("nonexistent");
            ShowMyVar(); // Here we get weird results.

            Console.WriteLine("\nNow again, but purely through .NET APIs.");

            Environment.SetEnvironmentVariable("myvar", "somevalue");
            ShowMyVar();

            Environment.SetEnvironmentVariable("myvar", String.Empty);
            ShowMyVar();

            Environment.GetEnvironmentVariable("nonexistent");
            ShowMyVar();


        }

        private static void ShowMyVar()
        {
            if (Environment.GetEnvironmentVariable("myvar") != null)
            {
                Console.WriteLine("myvar is set to \"{0}\"", Environment.GetEnvironmentVariable("myvar"));
            }
            else
            {
                Console.WriteLine("myvar is not set");
            }
            foreach (var x in Environment.GetEnvironmentVariables().Keys)
            {
                if (x.ToString() == "myvar")
                {
                    Console.WriteLine("iteration gives value of myvar as \"{0}\"", Environment.GetEnvironmentVariable("myvar"));
                    return;
                }
            }
            Console.WriteLine("iteration over environment does not yield myvar");
        }
    }
}

Upvotes: 3

Views: 2992

Answers (1)

Malcolm McCaffery
Malcolm McCaffery

Reputation: 2576

The reason for this:

Okay, sane results so far.  Now let's query an unrelated non-existent variable.

myvar is not set

.NET calls kernel32!GetEnvironmentVariableW ( "myvar", <pointer>, 128 ) and returns 0. GetLastError() is set to 203 = The system could not find the environment option that was entered.

This API is documented here http://msdn.microsoft.com/en-us/library/windows/desktop/ms683188(v=vs.85).aspx

iteration gives value of myvar as ""

Step 2 calls GetEnvironmentStringsW() which returns a pointer to the block of environment strings. MyVar is present here.

This API is documented here http://msdn.microsoft.com/en-us/library/windows/desktop/ms683187(v=vs.85).aspx

This information was obtained using Rohitab API monitor http://www.rohitab.com/apimonitor When using API monitor for a .NET EXE you may need to put a pause at beginning of your application, then attach API monitor later.

In this case enabled monitoring System Services -> Processes and Threads -> Process -> Kernel32.dll and unticked GetCurrentProcess()

The .NET source code called is here http://referencesource.microsoft.com/#mscorlib/system/environment.cs

Win32Native.ERROR_ENVVAR_NOT_FOUND = 203, this triggers the return null.

[System.Security.SecuritySafeCritical]  // auto-generated
        [ResourceExposure(ResourceScope.Machine)]
        [ResourceConsumption(ResourceScope.Machine)]
        public static String GetEnvironmentVariable(String variable)
        {
            if (variable == null)
                throw new ArgumentNullException("variable");
            Contract.EndContractBlock();

#if !FEATURE_CORECLR
            (new EnvironmentPermission(EnvironmentPermissionAccess.Read, variable)).Demand();
#endif //!FEATURE_CORECLR

            StringBuilder blob = new StringBuilder(128); // A somewhat reasonable default size
            int requiredSize = Win32Native.GetEnvironmentVariable(variable, blob, blob.Capacity); 

            if( requiredSize == 0) {  //  GetEnvironmentVariable failed
                if( Marshal.GetLastWin32Error() == Win32Native.ERROR_ENVVAR_NOT_FOUND)
                    return null;
            }

            while (requiredSize > blob.Capacity) { // need to retry since the environment variable might be changed 
                blob.Capacity = requiredSize;
                blob.Length = 0;
                requiredSize = Win32Native.GetEnvironmentVariable(variable, blob, blob.Capacity); 
            }
            return blob.ToString();
        }

In Win32 C++ with the following code works as you expect, unlike the .NET:

DWORD dwResult;
wchar_t buffer[128];

if (!SetEnvironmentVariableW(L"myvar", L""))
{
    dwResult = GetLastError();
    std::cout << "ERROR #" << dwResult << std::endl;

}

if (!GetEnvironmentVariableW(L"myvar", buffer,128))
{
    dwResult = GetLastError();
    std::cout << "ERROR #" << dwResult << std::endl;
}

std::wcout << "Buffer : '" << buffer << "'";

The following API calls occur:

ConsoleTest.exe!SetEnvironmentVariableW ( "myvar", "" ) TRUE        
    KERNELBASE.dll!RtlSetEnvironmentVar ( NULL, "myvar", 5, "", 0 ) STATUS_SUCCESS      
ConsoleTest.exe!GetEnvironmentVariableW ( "myvar", <pointer 1>, 128 )   0   0 = The operation completed successfully.   
    KERNELBASE.dll!RtlQueryEnvironmentVariable ( NULL, "myvar", 5, <pointer 1>, 128, <pointer 2> )  STATUS_SUCCESS  

When called from .NET 4 In this case I P/Invoked SetEnvironmentVariable and GetEnvironmentVariable directly after each other:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool SetEnvironmentVariable(string lpName, string lpValue);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern uint GetEnvironmentVariable(string lpName, [Out] StringBuilder lpBuffer, uint nSize);

StringBuilder buffer = new StringBuilder(128);

SetEnvironmentVariable("myvar", String.Empty);
uint result=GetEnvironmentVariable("myvar", buffer, 128);
if (result==0)
{
    Console.WriteLine(string.Format("Error: {0}", Marshal.GetLastWin32Error()));
}

This results in these API calls/results

 clr.dll->SetEnvironmentVariableW ( "myvar", "" )   TRUE    
       KERNELBASE.dll!RtlSetEnvironmentVar ( NULL, "myvar", 5, "", 0 )  STATUS_SUCCESS

    clr.dll->SetLastError ( ERROR_ENVVAR_NOT_FOUND )        
    clr.dll->SetLastError ( ERROR_ENVVAR_NOT_FOUND )            
    clr.dll->SetLastError ( ERROR_ENVVAR_NOT_FOUND )            
    clr.dll->SetLastError ( ERROR_ENVVAR_NOT_FOUND )            

    clr.dll!GetEnvironmentVariableW ( "myvar", <pointer 1>, 128 )   0   203 = The system could not find the environment option that was entered. 
       KERNELBASE.dll!RtlQueryEnvironmentVariable ( NULL, "myvar", 5, <pointer 1>, 128, <pointer 2> )   STATUS_SUCCESS  

In the .NET version the call KERNELBASE.dll!RtlQueryEnvironmentVariable succeeds

As far as I can see clr.dll is impacting the return value of GetEnvironmentVariableW.

If you start the .NET exe in WinDbg, and break after you set "somevalue"

Load .NET debugging extension

0:003> .loadby sos clr 

Show Process Environment Block (PEB) to find the address of Environment

0:003> !peb
PEB at 7f40d000
    InheritedAddressSpace:    No
    ReadImageFileExecOptions: No
    BeingDebugged:            Yes
    ImageBaseAddress:         00250000
    Ldr                       77d891e0
      etc...
    Environment:  **006b0c68**
      etc..

Search for location of our value in memory...

0:003> s -u **006b0c68** **006b0c68**+1000 "somevalue"
**006b1214**  0073 006f 006d 0065 0076 0061 006c 0075  s.o.m.e.v.a.l.u.

Set a read breakpoint on that memory location...

0:003> ba r 1 **006b1214**
0:000> du 6b1214
006b1214  "somevalue"
0:000> g
Breakpoint 1 hit
eax=00000000 ebx=006b1228 ecx=006b1214 edx=00000000 esi=00000000 edi=006b293a
eip=77ce37ad esp=003df094 ebp=003df108 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!RtlSetEnvironmentVar+0x516:
77ce37ad 83c102          add     ecx,2
0:000> du 6b1214
006b1214  ""
0:000> kv
ChildEBP RetAddr  Args to Child              
003df108 7774029e 00000000 01fe2b00 00000005 ntdll!RtlSetEnvironmentVar+0x516 (FPO: [SEH])
003df12c 005d04a3 01fe2b00 01fe1230 ed4737ca KERNELBASE!SetEnvironmentVariableW+0x47 (FPO: [Non-Fpo])
WARNING: Frame IP not in any known module. Following frames may be wrong.
003df1f8 73cc2552 0068e698 003df258 73ccf237 0x5d04a3
003df204 73ccf237 003df29c 003df248 73e18ad2 clr!CallDescrWorkerInternal+0x34
003df258 73ccff60 00000000 00000001 003df2b8 clr!CallDescrWorkerWithHandler+0x6b (FPO: [Non-Fpo])
003df2d0 73de671c 003df3cc efea5369 004e37fc clr!MethodDescCallSite::CallTargetWorker+0x152 (FPO: [Non-Fpo])
003df3f4 73de6840 01fe2a68 00000000 efea5495 clr!RunMain+0x1aa (FPO: [Non-Fpo])
003df668 73e23dc5 00000000 efea56e5 00250000 clr!Assembly::ExecuteMainMethod+0x124 (FPO: [1,149,0])
003dfb68 73e23e68 efea5b5d 00000000 00000000 clr!SystemDomain::ExecuteMainMethod+0x63c (FPO: [0,313,0])
003dfbc0 73e23f7a efea5c9d 00000000 00000000 clr!ExecuteEXE+0x4c (FPO: [Non-Fpo])
003dfc00 73e26b86 efea5ca1 00000000 00000000 clr!_CorExeMainInternal+0xdc (FPO: [Non-Fpo])
003dfc3c 7436ffcc eff9e4d3 7573980c 74360000 clr!_CorExeMain+0x4d (FPO: [Non-Fpo])
003dfc74 743ebbb7 003dfc8c 743ebbcc 00000000 mscoreei!_CorExeMain+0x10a (FPO: [0,10,4])
003dfc7c 743ebbcc 00000000 00000000 003dfc98 MSCOREE!_CorExeMain_Exported+0x77 (FPO: [Non-Fpo])
003dfc8c 7573919f 7f40d000 003dfcdc 77cda8cb MSCOREE!_CorExeMain_Exported+0x8c (FPO: [Non-Fpo])
003dfc98 77cda8cb 7f40d000 eea2cea9 00000000 KERNEL32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
003dfcdc 77cda8a1 ffffffff 77ccf663 00000000 ntdll!__RtlUserThreadStart+0x20 (FPO: [SEH])
003dfcec 00000000 743ebb40 7f40d000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
0:000> !clrstack
OS Thread Id: 0x3464 (0)
Child SP       IP Call Site
003df140 77ce37ad [InlinedCallFrame: 003df140] 
003df13c 005d04a3 *** WARNING: Unable to verify checksum for ConsoleApplicationTest.exe
DomainBoundILStubClass.IL_STUB_PInvoke(System.String, System.String)
003df140 005d0106 [InlinedCallFrame: 003df140] ConsoleApplication1.Program.SetEnvironmentVariable(System.String, System.String)
003df1a4 005d0106 ConsoleApplication1.Program.Main(System.String[]) [c:\Users\mccafferym\Documents\Visual Studio 2013\Projects\ConsoleApplicationTest\ConsoleApplicationTest\Program.cs @ 32]
003df378 73cc2552 [GCFrame: 003df378] 
0:000> g

We now hit breakpoint when we are reading back the value...

Breakpoint 1 hit
eax=0000003d ebx=006b1212 ecx=006b1214 edx=01fe2b0a esi=006b1208 edi=00000001
eip=77cda2f7 esp=003def5c ebp=003def74 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!RtlpScanEnvironment+0xa7:
77cda2f7 75f7            jne     ntdll!RtlpScanEnvironment+0xa0 (77cda2f0) [br=0]
0:000> kv
ChildEBP RetAddr  Args to Child              
003def74 77cda138 01fe2b0a 003df000 00000080 ntdll!RtlpScanEnvironment+0xa7 (FPO: [Non-Fpo])
003defc8 77732b88 00000000 01fe2b00 00000005 ntdll!RtlQueryEnvironmentVariable+0xa7 (FPO: [SEH])
003defec 005d0682 01fe2b00 003df000 00000080 KERNELBASE!GetEnvironmentVariableW+0x39 (FPO: [Non-Fpo])

Tracing execution onwords from here, we see error is set here:

KERNELBASE!GetEnvironmentVariableW+0x52:
77732ba9 c20c00          ret     0Ch
006f0682 8b4d98          mov     ecx,dword ptr [ebp-68h] ss:002b:0033f040=004ee698
006f0685 c6410801        mov     byte ptr [ecx+8],1         ds:002b:004ee6a0=00
006f0689 833d64a32f7400  cmp     dword ptr [clr!g_TrapReturningThreads (742fa364)],0 ds:002b:742fa364=00000000
006f0690 7407            je      006f0699                                [br=1]
006f0699 c7458000000000  mov     dword ptr [ebp-80h],0 ss:002b:0033f028=006f0682
006f06a0 8945ac          mov     dword ptr [ebp-54h],eax ss:002b:0033f054=0033f288
006f06a3 e801545e73      call    clr!StubHelpers::SetLastError (73cd5aa9)

If this behaviour required changing I'd recommend raising a support case with Microsoft.

Upvotes: 4

Related Questions