-2

I had written a library (which worked fine) in C# for running Win32 functions. Such as PInvoking CredUIPromptForWindowsCredentials,CredUnPackAuthenticationBuffer,CredPackAuthenticationBuffer, so one method call can return NetworkCredentials without you doing all the credential packing/unpacking and error checking youself. And similarly classes and methods for creating/listing/changing/removing local users and local groups from a computer using NetUser... and NetGroup... functions

//ToolsCS.cs

public static class Credentials{
    public static NetworkCredential GetCredentials(string promtpCaption, string promptMessage, string inUsername, string inDomain, string inPassword){...}
    public static NetworkCredential GetCredentials(string promtpCaption, string promptMessage){...}
}
public class User{
    public static User[] GetUsers(string computername){...}
    public static bool Exists(string computername,string username){...}
    public static void Create(string computername,User user){...}
    public static void Delete(string computername,string username){...}
    public static void Set(string computername,string username,User user){...}
    public string Name;
    public string DisplayName;
    public string Description;
}

This all works great for any other C# library or app that calls them. Now I am writing a couple apps in C, which are using the same functions i just made my C# library for, so why not make it in C to use for these C apps, make writing the app itself simpler.

//ToolsC.c

extern "C" __declspec(dllexport) BOOL __stdcall AccGroupList(wchar_t* system,wchar_t** names,DWORD* count);
extern "C" __declspec(dllexport) BOOL __stdcall AccGroupExists(wchar_t* system,wchar_t* name);
extern "C" __declspec(dllexport) BOOL __stdcall AccGroupCreate(wchar_t* system,wchar_t* name);
extern "C" __declspec(dllexport) BOOL __stdcall AccGroupSet(wchar_t* system,wchar_t* name);
extern "C" __declspec(dllexport) BOOL __stdcall AccGroupRemove(wchar_t* system,wchar_t* name);
extern "C" __declspec(dllexport) BOOL __stdcall AccGroupAddMember(wchar_t* system,wchar_t* group,wchar_t* member);

This all workds great for the apps im writing in C. But the trouble I get is when I decided to try calling these C functions i wrote from C#

//SomeApp.cs
class SomeClass{
    [DllImport(@"ToolsC.dll",SetLastError=true)]
    public static extern bool AccGroupList(
        [MarshalAs(UnmanagedType.LPWStr)]                       string system,
        [MarshalAs(UnmanagedType.LPArray,SizeParamIndex=2)] ref string[] names,
                                                            ref int count);
    ...
}

When debugging the C# code, it successfully steps into my exported C dll function it crashes with a Access Violation error when it makes the call to NetGroupEnum (which in documentation says that the system allocates a buffer, not the caller) the same error crashes the C# app when it calls my C function the calls CredUIPromptForWindowsCredentials (which also says the system allocates a buffer). It's not that the Win32 functions return and then I get error when accessing the buffer it gave me, It's that the debugger gives me an error box without returning from that function. For AccGroupExists, it uses NetUserGetInfo function, and only gets the user's name, returns wihtout crashing, this function fills a buffer you give it, not a "system allocated buffer".

I already tried changing the my exported C functions to have different parameter types, and allocating any strings inside the C function and copying the passed C# strings into the wchar_t* before calling the Win32 functions. And I tried changing the PInvoke parameter types.

So it seems like I have to settle for doing C#->Win32 for C#, and C->Win32 for C. But I cant use C#->C->Win32.

Is there something I can do in C# like setting the memory security or process access rights before I make calls to my C dll that will allow the Win32 functions to allocate memory without crashing the application?

EDIT

Here is a function in the C library, which works fine for me when called in another C app. But fails at NetLocalGroupEnum from C#.

extern "C" __declspec(dllexport) BOOL __stdcall AccGroupList(wchar_t* system,wchar_t** names,DWORD* count){
    GROUP_INFO_0* buffer;
    DWORD readcount=0;
    DWORD totalcount=0;
    DWORD size=0;
    //C# app crashes here
    error=NetLocalGroupEnum(system,0,(LPBYTE*)&buffer,MAX_PREFERRED_LENGTH,&readcount,&totalcount,NULL);
    if(error!=NERR_Success){
        SetLastError(error);
        return FALSE;}
    if((names=(wchar_t**)CoTaskMemAlloc(sizeof(wchar_t*)*readcount))==NULL){
        NetApiBufferFree(&buffer);
        SetLastError(ERROR_NOT_ENOUGH_MEMORY);
        return FALSE;}
    for(int i=0;i<readcount;i++){
        size=wcslen(buffer[i].grpi0_name);
        if((names[i]=(wchar_t*)CoTaskMemAlloc(2*size))==NULL){
            for(int j=0;j<i;i++){
                size=wcslen(names[j]);
                wmemset(names[j],0,size);
                CoTaskMemFree(names[j]);}
            NetApiBufferFree(&buffer);
            CoTaskMemFree(names);
            SetLastError(ERROR_NOT_ENOUGH_MEMORY);
            return FALSE;}
        
        wmemset(names[i],0,size);
        wcscpy(names[i],buffer[i].grpi0_name);}
    NetApiBufferFree(&buffer);
    *count=readcount;
    return TRUE;}
Robot
  • 1
  • 1
  • `[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex=2)]` Also how are you calling this code, are you preallocating the array buffer in C#? – Charlieface May 07 '23 at 02:16
  • @Charlieface the array gets allocated in the C function after calling the external windows function. But the array is not an argument to the call to the netapi32 library. – Robot May 07 '23 at 04:48
  • 1
    *"Crash"* means a lot of different things to a lot of people. What's the *specific* error you're observing. Also, if a function allocates resources, then those parameters should be `out` rather than `ref`. – IInspectable May 07 '23 at 05:51
  • @IInspectable if i run it from command prompt, it "crashes" as in it not only exits the the exe, but also closes the command prompt i ran it from. When using debugger, even if I wrap the pinvoke call in a try/catch, the debugger shows the "Access Violation" error, and then immediately closes the debugging session. I had tried, decorating the C function to have out for the parameters that are written to, and tried both ref and out for the pinvoke in c# – Robot May 07 '23 at 06:05
  • You can't allocate it in C as the C# side has no way of deallocating it. You must allocate and deallocate it on the same side, ie C# . And once you have done that you should do `[Out] string[] names` not `ref string[] names` ie remove the `ref` – Charlieface May 07 '23 at 10:01

1 Answers1

-1

What you want from the C# side is get back a list of strings, the list being itself allocated by the native side, so you have to declare a pointer to an array (a pointer) of strings (a pointer), so the declaration should pass a triple pointer (plus there are some other issues in the code):

extern "C" BOOL __stdcall AccGroupList(wchar_t* system, wchar_t*** names, DWORD * count) {
    GROUP_INFO_0* buffer;
    DWORD readcount;
    DWORD totalcount;
    wchar_t** readnames;

    *names = NULL;
    *count = 0;
    DWORD error = NetLocalGroupEnum(system, 0, (LPBYTE*)&buffer, MAX_PREFERRED_LENGTH, &readcount, &totalcount, NULL);
    if (error != NERR_Success) {
        SetLastError(error);
        return FALSE;
    }

    if ((readnames = (wchar_t**)CoTaskMemAlloc(sizeof(wchar_t*) * readcount)) == NULL) {
        NetApiBufferFree(buffer);
        SetLastError(ERROR_NOT_ENOUGH_MEMORY);
        return FALSE;
    }

    for (DWORD i = 0; i < readcount; i++) {
        size_t size = wcslen(buffer[i].grpi0_name) + 1; // + 1 to account for terminating 0
        if ((readnames[i] = (wchar_t*)CoTaskMemAlloc(2 * size)) == NULL) {
            for (DWORD j = 0; j < i; j++) { // j++ instead of i++
                CoTaskMemFree(readnames[j]);
            }

            NetApiBufferFree(buffer);
            CoTaskMemFree(readnames);
            SetLastError(ERROR_NOT_ENOUGH_MEMORY);
            return FALSE;
        }

        wcscpy(readnames[i], buffer[i].grpi0_name);
    }

    NetApiBufferFree(buffer); // free buffer, not pointer to it
    *names = readnames;
    *count = readcount;
    return TRUE;
}

And the C# should be like this (each string is unicode, so you must use ArraySubType, and variable parameters are out, since fully given by the native side):

[DllImport(@"ToolsC.dll", SetLastError = true)]
public static extern bool AccGroupList(
    [MarshalAs(UnmanagedType.LPWStr)] string system,
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2, ArraySubType = UnmanagedType.LPWStr)] out string[] names,
    out int count);
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • How would C# free that memory? Would have thought it better to just allocate and free on the C# side only. – Charlieface May 07 '23 at 11:24
  • @Charlieface That would probably require passing a [`SAFEARRAY`](https://learn.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-safearray), holding [`BSTR`](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr)s. With that either side knows which allocator to use. An easier approach would be to create a mixed-mode interop assembly, using C++/CLI, that the .NET code can use to call into the native code. – IInspectable May 07 '23 at 11:40
  • @Charlieface - As long as you use the COM allocator, it's fine. On recent version of Windows, they're kinda all the same anyway https://stackoverflow.com/a/36423272/403671 on Linux (dotnet core) you can use malloc https://github.com/dotnet/runtime/issues/10748 – Simon Mourier May 07 '23 at 12:33
  • It was a platform issue, when I had the C and C# both in 32bit, it failed, but when I set them to both 64bit it worked. Not really sure why, since the NetGroupEnum is in netapi32, a 32bit library. – Robot May 07 '23 at 20:24
  • @Robot - It's clearly not (only) a platform issue – Simon Mourier May 07 '23 at 21:02
  • I was recreating the solution in visual studio to so I could put it on github, and post a link here. When I was making the first solution i fiddled with the project files for the C librarys a lot, but this time all i did was set everything to target x64, and it worked. – Robot May 08 '23 at 00:44