Reputation: 13539
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
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