Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GSSAPI support #3

Open
DMHP opened this issue Jun 16, 2016 · 28 comments
Open

GSSAPI support #3

DMHP opened this issue Jun 16, 2016 · 28 comments

Comments

@DMHP
Copy link

DMHP commented Jun 16, 2016

Hi antiduh, I'm following this in the github. In kerberos we have some steps like 1. Client requesting the TGT from KDC 2. Client getting the TGT 3.Client requesting SGT from KDC by using TGT, to access specific service 4. Client getting the SGT 5. Send SGT to resource server 6. Access the resource. Can you please tell me how do you handle the 3rd and 4th steps in the github.com/antiduh/nsspi/blob/master/TestClient/ClientForm.cs. Thanks.:)

@antiduh
Copy link
Owner

antiduh commented Jun 16, 2016

The SSPI API exposes this through the authentication cycle. Keep in mind that SSPI is supposed to be provider agnostic - the calling code doesn't need to know whether it's working with NTLM tokens, kerberos tokens, or some other system's tokens.

The authentication cycle assumes that you already have access to some sort of credential - in Kerberos's case, the credential would be the ticket granting ticket (TGT).

So, on the client side, I get a handle to my user's TGT - which was created when the user logged on. On the server side, the service gets a handle to the TGT of whatever principle it's running on, which was created when the service started.

From there, the authentication cycle begins.

On the client side, there are three pieces of data involved in the authentication cycle.

  • The token we last received from the server.
  • The status of the client's half of the authentication cycle.
  • The token that we are to transmit to the server so that the server may progress its half of the authentication cycle.

Note that the client usually starts, and so, on the first invocation we provide null for the server's previous token.

On the server side, there are the same three pieces of data:

  • The token we last received from the client.
  • The status of the client's half of the authentication cycle.
  • The token that we are to transmit to the client so that the client may progress its half of the authentication cycle.

So the authentication cycle usually progresses as follows:

  • The client invokes InitializeSecurityContext (ISC for short), providing null for the previous token from the server.
  • ISC outputs the token to write to the server ('the output token'), and returns status 'Continue'.
  • The client transmits his output token to the server.
  • The server takes the received client token, and provides it as input to AcceptSecurityContext (ASC for short).
  • ASC outputs the token to write to the client, and returns status 'Continue'.
  • The client receives the servers token.
  • The client invokes ISC, providing the received token.
  • ISC returns a new token to output to the server, and returns status 'Continue'.
  • Client transmits the token to the server.
  • Server receives token from the client
  • Server calls ASC, providing the received client token as input.
  • ...

Depending on how much the two sides need to talk, there could be many iterations of the above cycle. Eventually, one side will return a status 'OK'. That means that, from that side's perspective, it has everything it needs. However, it will still have a token to write to the other side.

So the above could continue in one of the two following manners:

  • The client calls ISC with the token received from the server.
  • ISC outputs a token to write to the server and returns status 'OK'. The client is satisfied with its half of the authentication cycle, but we still have a token to write to the server.
  • The client writes the token to the server.
  • The server calls ASC with the token received from the client.
  • ASC outputs null and returns status 'OK'.
  • The authentication cycle is complete.

or

  • Server receives token from the client
  • Server calls ASC, providing the received client token as input.
  • ASC outputs a token to write to the client and returns status 'OK'. The server is satisfied with its half of the authentication cycle, but we still have a token to write to the client.
  • The server writes the token to the client.
  • The client calls ISC with the received token.
  • ISC outputs null and returns status 'OK'.
  • The authentication cycle is complete.

In short: If ISC or ASC returns a token, then you have to write that to the other side, regardless of what status ISC or ASC return.

So how does kerberos fit into this design?

Well, the client doesn't have to specify the principle name of the server he wants to connect to; it's an optional parameter to the ClientContext class. So how would the client find out who the server is in order to get SGT from the KDC?

Well, you ask him.

So:

  • The first time the client invokes ISC, it returns a token that is sent to the server asking him to identify himself.
  • The server takes that token, calls ASC with it, and ASC returns a token that provides a response.
  • The client takes that response, calls ISC.
  • Now that the client's SSPI API knows what server it's talking to, it behind the scenes talks to the KDC to ask for the SGT.
  • The client's call to ISC returns a token containing the SGT.
  • The client sends the token containing the SGT to the server.
  • The server calls ASC with that token containing the SGT
  • The server now has proof the client is who he says he is, and authentication succeeds.
  • The server's call to ASC returns a token indicating success.
  • The server sends that token to the client.
  • The client calls ISC with that token indicating success.
  • ISC outputs no token and returns status 'OK'.
  • Done.

That process may be more or less complicated, depending on circumstances and parameters. If you use the negotiate package, then the first couple tokens to be exchanged between the client and server will be queries about what security packages they support, and which is the most secure package that they both have. After they settle on the security package, the tokens will be about exchanging principle information and then finally tickets.

You can also initialize the ClientContext with a parameter indicating the principle name of the service to connect to - in which case, the client can probably skip a few steps, contact the KDC before sending a single packet, and be done in one or two packets.

However, please keep in mind that the SSPI API uses a custom protocol - it carries kerberos tickets in its payload, but it is not simply the kerberos protocol itself, or at least, you should not assume it is.

From the caller's perspective, it's an opaque protocol: you receive a packet, call the function, get an output packet, send it to the other side .. they receive a packet, call a function, get an output to send back ... and so on until the cycle is complete.

So far we've discussed the SSPI API - which is a win32 API, and is not my software. the InitializeSecurityContext win32 function is wrapped by my ClientContext.Init() method, and AcceptSecurityContext by my ServerContext.Init() method.

To answer your direct question: we start the cycle in the ClientForm when the user clicks the connect button:

    private void connectButton_Click( object sender, EventArgs e )
    {
        if( string.IsNullOrWhiteSpace( this.serverTextBox.Text ) )
        {
            MessageBox.Show( "Please enter a server to connect to" );
        }
        else
        {
            try
            {
                this.connection.StartClient( this.serverTextBox.Text, (int)this.portNumeric.Value );
                this.initializing = true;
                // ###############################
                DoInit();
                // ###############################

We call the DoInit() function the first time when the button is clicked. That performs the first call to InitializeSecurityContext and then using our own little toy custom protocol, we send the token to the server:

    private void DoInit()
    {
        SecurityStatus status;
        byte[] outToken;

        status = this.context.Init( this.lastServerToken, out outToken );

        if( status == SecurityStatus.ContinueNeeded )
        {
            Message message = new Message( ProtocolOp.ClientToken, outToken );
            this.connection.Send( message );
        }
        else if( status == SecurityStatus.OK )
        {
            this.lastServerToken = null;
            this.initializing = false;
            this.connected = true;
            UpdateButtons();
        }
    }

When we receive a packet back from the server, we'll look at what kind of packet it is, and if it's an authentication cycle packet, we'll call DoInit() again:

   private void connection_Received( Message message )
    {
        this.Invoke( (Action)delegate()
        {
            if( message.Operation == ProtocolOp.ServerToken )
            {
                if( initializing )
                {
                    // ###############################
                    this.lastServerToken = message.Data;
                    DoInit();
                    // ###############################                    
    }

Hopefully, that answers your question.

Please keep in mind the TestClient and TestServer are meant to be demo projects - not production code. Please think twice before copy-and-pasting the code.

Additionally, keep in mind that the SSPI API just gives you bytes to transmit, and its up to you to decide how to transmit those bytes.

If you look in the NSspi project, there's a Program.cs file that shows how to perform the authentication cycle locally, with the client and server running in the same process. The tokens are 'transmitted' by in memory - we just directly call the next function with the bytes returned from the previous function.

@antiduh
Copy link
Owner

antiduh commented Jun 16, 2016

Please let me know if the above comment answers your question, and if so, I'll close this issue.

@DMHP
Copy link
Author

DMHP commented Jun 16, 2016

Thanks antiduh for the detailed explanation. I got it now. I'm just following your implementation to get an idea about how SSPI works as I have to develop a C#.net client to consume one of our server's (java) kerberos secured rest proxy with Active Directory as the KDC. Please close the issue.

@antiduh
Copy link
Owner

antiduh commented Jun 16, 2016

Does it implement GSSAPI? In that case, you can use the SSPI API to perform authentication, which means with some modifications you would be able to use Nsspi to implement it.

The following page outlines how to invoke the SSPI API to be compatible with GSSAPI:

https://msdn.microsoft.com/en-us/library/windows/desktop/aa380496(v=vs.85).aspx

If you modified nsspi to take those changes into account, it should work.

@DMHP
Copy link
Author

DMHP commented Jun 16, 2016

Yes. The requirement is to implement it using GSSAPI. So as I'm developing the client in .NET I can use SSPI, that means some modifications to Nsspi as you said as the client and I can directly use GSSAPI in java server side for this communication flow. Do we need to consider SPNEGO spec [1] separately with our implementation or GSSAPI and SSPI covers that spec too?

[1] https://tools.ietf.org/html/rfc4559

@antiduh
Copy link
Owner

antiduh commented Jun 16, 2016

Yeah, that sounds like it should work, so long as Microsoft's claim to be GSSAPI compatible is true.

Note, when you go digging through the nsspi code, you're going to run across Constrained Execution Regions (CERs). I've had to manually implement them because the SSPI handle isn't compatible with .Net's SafeHandle class, which normally handles a lot of magic for you in a very concise way.

I have a fairly verbose blog article that provides a ground up explanation of CERs, why they're necessary etc.

Start here: https://antiduh.com/blog/?q=node/4

@DMHP
Copy link
Author

DMHP commented Jun 16, 2016

Thanks antiduh. I will follow the things and will start the implementation. :)

@DMHP
Copy link
Author

DMHP commented Jun 17, 2016

Hi antiduh,
I'm facing a problem here. I used Nsspi dll and created two C# console apps as client and server and it was working fine. But when I'm using the same C# client with java server, the server is accepting the TGT and creates the server token. But when generating the SGT in client side it fails.I mean when the client calls to the Init method for the second time it causes to an exception. Do you have any idea why this is happening. Thanks.

@antiduh
Copy link
Owner

antiduh commented Jun 17, 2016

I'm not sure how much I'm going to be able to help here. My only advice would be to read the Win32 SSPI documentation thoroughly, dig into what the errors mean, and make sure you follow the above MSDN article on GSSAPI interoperability to the letter.

@DMHP
Copy link
Author

DMHP commented Jun 21, 2016

Hi, what will be the reason for getting this "The specified principle is not known in the authentication system." error in nsspi. I got it when I'm validating the server token which I got from server side after accepting the TGT of the client. Do you have any idea?

@antiduh
Copy link
Owner

antiduh commented Jun 21, 2016

It probably means that the client doesn't recognize the servers principle name (SPN). If I recall that documentation correctly, you need to perform translation of some of the received tokens before passing to InitializeSecurityContext because Microsoft's kerberos implementation doesn't understand some SPN formats.

@DMHP
Copy link
Author

DMHP commented Jun 22, 2016

Hi antiduh,
I could implement my use case to communicate the C# client (SSPI based) and Java server (GSSAPI based). Thank you very much for the support provided. :)

@antiduh
Copy link
Owner

antiduh commented Jun 22, 2016

That's really good news, I'm glad you were able to get it to work. SSPI can be a right pain in the neck sometimes.

Would you be willing to contribute back your code? You don't have to (my license says you can do whatever you want with my code and your code), but I'd like to save folks in the future the trouble of reimplementing it themselves.

@antiduh antiduh changed the title SGT Token GSSAPI support Jun 22, 2016
@DMHP
Copy link
Author

DMHP commented Jun 23, 2016

Yes antiduh, Sure. I will contribute my code. I have a tight schedule till 1st July. Will contribute the code ASAP. thanks

@k2ibegin
Copy link

@DMHP Can you please contribute (if possible). i have to implement the same for my proxy server authentication. Initially i assumed to support only NTLM but I guess i would have to take both approaches of kerberos and NTLM, as NTLM usually is a fall back mechanism in case kerberos is not available !

@antiduh
Copy link
Owner

antiduh commented Jan 20, 2017

Hey @DMHP - any news? If you just want to send me a code dump I can sort through it, clean it up; i'd just be happy to have your contributions to add to the project.

@k2ibegin
Copy link

k2ibegin commented Mar 1, 2017

hi @antiduh , I could successfully support the negotiate and ntlm based authentication (win auth) for both proxy server and subsequently win server. However, at customer site the first authentication cycle fails with response coming back as just NEGOTIATE in proxy authenticate header. Ideally it should contain server token as well. Usually I take this token and continue auth cycle. However, this works in my local setup with tmg proxy and iis server running on vms. Customer setup is diff in the sense that they have AD and sso setup I believe. I am just wondering that if I dont use kerberos security package and just sticking to win base auth (negotiate) then would my code still work irrespective of AD DOMAIN setup on customer side? If this is suposed to work fine then I should look for sm other bugs, however, at this point I dont know if there are any other bugs leading to this behavior. also please note that basic auth just works fine at customers place.... Thanks

@antiduh
Copy link
Owner

antiduh commented Mar 1, 2017

Negotiate is a 'meta' package; it's not actually a security system in its own. The real packages are ones like Kerberos and NTLM.

When you specify Kerberos/NTLM, you're stating that you have a hard requirement for one or the other. If you pick Kerberos and the other end doesn't support it, the context will probably fail.

If you use Negotiate, then both ends will automatically figure out whether they should use Kerberos or NTLM. To you, you'll just be passing back and forth a bunch of opaque tokens and eventually the auth cycle will complete; to them, they're first exchanging tokens to figure out what security package (NTLM, Kerberos) they both support and in what order they prefer them, then which one they'll decide on, then the real tokens for that package.

If you want it to work irrespective of customer configuration, use Negotiate; it'll pick Kerberos or NTLM for you automatically. IIRC, most modern AD sites use Kerberos.

@k2ibegin
Copy link

k2ibegin commented Mar 1, 2017

So it means that if I am using negotiate mechanism, and server supports kerberos then server will automatically choose kerberos. But my question is what happens when my code does not support Kerberos. I am not sure about on my side of the code because I am usually giving the target server a proxy server, and uses this API to get the tokens (using windows credentials, domain name etc). I am wondering if my code needs to communicate with KDC (AD in this case i assume) seperately? In a nutshell I am not clear on how client really knows about the KDC ? Is it via the service principle name (SPN). I mean I can print the SPN after this line of code

clientCred = new NSspi.Credentials.ClientCredential(packageName, username, password, domain);

clientCred.principleName gives me the SPN.

@antiduh
Copy link
Owner

antiduh commented Mar 1, 2017

The question comes down to how your process is acquiring credentials.

In the standard situation, you have a client (user) that is part of the same AD as the server. The client logs in, and in doing so, creates kerberos credentials. The server is logged in, and in doing so, creates its kerberos credentials.

In the normal circumstance, the program that uses nsspi is only acquiring a handle to existing credentials that SSPI already knows about - credentials that were assigned to your process when the process was started.

In your circumstance, it appears that you're creating credentials by calling some sort of kerberos logon - the constructor ClientCredential( packageName, username, password, domain); doesn't exist in vanilla nsspi. I don't know how that constructor works (or any of your other changes), so I'm not sure I can really answer your question.

The client knows about the KDC via system configuration. If you are creating credentials via username/pass, then under the hood, the API is talking to the KDC to get the kerberos creds.

I don't know if the code you're using (that ClientCredential constructor overload) is acquiring kerberos creds or NTLM creds. If you're accidentally acquiring NTLM creds, then its possible that auth is failing because the client and the server don't have compatible SSPs.

@k2ibegin
Copy link

k2ibegin commented Mar 1, 2017

Well actually you are right. I had to tweak the code a little as the requirement was that one should be able to provide the winauth credentials (in case the machine is not in a domain but the process has to authenticate with a remote proxy or web server ) with some sort of pop up screen. This was a pain because the credentials of these two machines might be different as they are not monitored by some domain controller. However, this is just one use case which we needed for our testing. We create an HttpRequest class which different clients call to use my API to get the authentication. However, in production case there are no user id, passwords passed internally as they are null. Inside the NSSPI code we check for this and the 5th argument to function AcquireCredentialsHandle is null ptr as in the original NSSPi code ..

if (username == null || username.Length == 0)
{
pnt = IntPtr.Zero;
}

            status = CredentialNativeMethods.AcquireCredentialsHandle(
               null,
               packageName,
               use,
               IntPtr.Zero,
               pnt,
               IntPtr.Zero,
               IntPtr.Zero,
               ref this.Handle.rawHandle,
               ref rawExpiry
           );

Now having said that

As per my understanding in usual circumstance (also in my case) the SSPi takes care of acquiring credentials for my process and hence kerberos OR NTLM, it should be fine ? (given that I am still using the NSspi.Credentials.ClientCredential(packageName);, which is the original NSSPI code !

@antiduh
Copy link
Owner

antiduh commented Mar 1, 2017

I'll admit, I'm starting to get a little confused by the back and forth between how you normally use nsspi, and how its used with this customer (and is breaking).

If you are attempted to use nsspi on a client to authenticate with a server, then both the client and server must be part of the same kerberos domain. So ultimately your client has to talk to kdcserver.domain.com, and the server ultimately has to talk to kdcserver.domain.com; they need to have TGTs from the same KDC.

I'm not sure what other values pnt is set to other than null; if you wanted to provide a username and pass, that's not the way to do it; you'd have to assemble an instance of the SEC_WINNT_AUTH_IDENTITY structure to acquire creds using username/pass/domain.

It would really help if you could fork nsspi and commit your changes so I can see how you're using it, then you could tell me what parts are failing.

@k2ibegin
Copy link

k2ibegin commented Mar 2, 2017

I am sorry for the confusion. I am assembling the instance of SEC_WINNT_AUTH_IDENTITY structure as you have mentioned for that purpose.

So basically I was given a task to have following 2 situations

  1. Client and server should be part of same domain and hence win auth works using sspi automatically.

  2. Client and server does not belong to same domain. In this case I realized that it is still possible to pass on the credentials from the client side ( by asking user for it e.g. a pop up auth win form). For this case I tweaked the code as MSDN mention about assembling the instance of SEC_WINNT_AUTH_IDENTITY structure. Therefore I am doing it like this in ClientCredential.cs

     private void Init( CredentialUse use, string username ="", string password = "", string domain="")
     {
         string packageName;
         TimeStamp rawExpiry = new TimeStamp();
         SecurityStatus status = SecurityStatus.InternalError;
    
         SEC_WINNT_AUTH_IDENTITY sec_winnt_auth_identity = new SEC_WINNT_AUTH_IDENTITY
         {
             User = (username == null) ? "" : username,
             UserLength = (username == null) ? 0 : username.Length,
             Password = (password == null) ? "" : password,
             PasswordLength = (password == null) ? 0 : password.Length,
             Domain = (domain == null || domain == "") ? null : domain,
             DomainLength = (domain == null || domain == "") ? 0 : domain.Length,
             Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE
         };
    
         // -- Package --
         // Copy off for the call, since this.SecurityPackage is a property.
         packageName = this.SecurityPackage;
    
         this.Handle = new SafeCredentialHandle();
    
         // The finally clause is the actual constrained region. The VM pre-allocates any stack space,
         // performs any allocations it needs to prepare methods for execution, and postpones any 
         // instances of the 'uncatchable' exceptions (ThreadAbort, StackOverflow, OutOfMemory).
         RuntimeHelpers.PrepareConstrainedRegions();
         try { }
         finally
         {
            
             IntPtr pnt = Marshal.AllocHGlobal(Marshal.SizeOf(sec_winnt_auth_identity));
    
             // Copy the struct to unmanaged memory.
             Marshal.StructureToPtr(sec_winnt_auth_identity, pnt, false);
             //SEC_WINNT_AUTH_IDENTITY locTst = new SEC_WINNT_AUTH_IDENTITY();
            
             //locTst = (SEC_WINNT_AUTH_IDENTITY)Marshal.PtrToStructure(pnt, typeof(SEC_WINNT_AUTH_IDENTITY));
             if (username == null || username.Length == 0)
             {
                 pnt = IntPtr.Zero;
                 Console.WriteLine("***************THIS IS NULL*****************");
             }
                 
             
             status = CredentialNativeMethods.AcquireCredentialsHandle(
                null,
                packageName,
                use,
                IntPtr.Zero,
                pnt,
                IntPtr.Zero,
                IntPtr.Zero,
                ref this.Handle.rawHandle,
                ref rawExpiry
            );
            Marshal.FreeHGlobal(pnt);
             
         }
    
         if ( status != SecurityStatus.OK )
         {
             throw new SSPIException( "Failed to call AcquireCredentialHandle", status );
         }
    
         this.Expiry = rawExpiry.ToDateTime();
     }
    

So as you can see that if the client code passes the values of user credentials ( in case the client is not on the same domain and IT IS NTLM based authentication ) then this code works fine. However, if client chooses to use SSPI for automatically figuring out the user credentials then the code works BAU (The original NSSPI flow).

I hope It is more clear now. I tested this in my local environment , steps were as following
Flow 1:

  1. my machine has its local windows credentials which do not match with the machine where proxy server is running. Both my machine and proxy server machines are not in any domain.
  2. Proxy Server asks for the NTLM, Negotiate based authentication and I pass NSSPI the user credentials (via a config read) and assemble the SEC_WINNT_AUTH_IDENTITY structure and hence the token is generated.

Flow 2:
In my environment: The code also works fine when my machine and the machine where web server is running are on the same domain. ( with AD setup)

At Customer environment:
However, when I test at the customer's environment. They do have the all machines set up in an AD domain.
My code works fine for the first cycle, which is

  1. Get Request to Proxy server
  2. Response from Proxy with supported protocols (negotiate, Kerberos, NTLM, Basic)
  3. My code picks Negotiate package and generate the token and send to the proxy Server by populating the Proxy-Authorization header
  4. Server sends back the response with new serverToken in Proxy-Authenticate header.
  5. My code picks this token, and passes to the _clientCtx.Init(serverToken, out clientToken);
  6. step 5 results into NSSPI Exception with error code
    "Operation not supported" 0x80090302

So basically I am wondering if this was the issue with the authentication and credentials, then should not it have failed at the first cycle itself?

I know that choosing Negotiate means that client and server automatically choose the best mechanism supported by both ends, which happens to be in this case Kerberos. My doubt is if i need to do something different on my side to support Kerberos? I am suspecting that this is failing because Server supports Kerberos but my code does not ?

Thanks for reading this long post !!

I hope I made it more clear than last time.

@pradeeptac
Copy link

@antiduh can nsspi be used to implement kerberos authentication in iis .

@hambonewa
Copy link

Does it implement GSSAPI? In that case, you can use the SSPI API to perform authentication, which means with some modifications you would be able to use Nsspi to implement it.

The following page outlines how to invoke the SSPI API to be compatible with GSSAPI:

https://msdn.microsoft.com/en-us/library/windows/desktop/aa380496(v=vs.85).aspx

If you modified nsspi to take those changes into account, it should work.

It seems this is the only mention of GSSAPI within these posts. Can you be more specific regarding "to take those changes into account"?

BTW, I successfully get a negotiated bind between client/server. The encrypt/decrypt fail. When I look at the byte array I receive from the server (150 bytes in total), it is nothing like what your code describes as:

124 /// The structure of the returned data is as follows:
125 /// - 2 bytes, an unsigned big-endian integer indicating the length of the trailer buffer size
126 /// - 4 bytes, an unsigned big-endian integer indicating the length of the message buffer size.
127 /// - 2 bytes, an unsigned big-endian integer indicating the length of the encryption padding buffer size.
128 /// - The trailer buffer
129 /// - The message buffer
130 /// - The padding buffer.

My byte array in decimal is:
1, 0, 0, 0, 202, 100, 56, 26, 175, 164, .....

I'm guessing GSSAPI is the issue.

@antiduh
Copy link
Owner

antiduh commented Nov 16, 2018

@hambonewa - GSSAPI is its own standard, separate from SSPI. SSPI can be used compatibly with GSSAPI, but with some tweaks. There's documentation on MSDN that describes it.
I've not a test environment to mess around with GSSAPI, so I've not implemented direct support for it in NSSPI.

About the message handling methods - those structures are defined by nsspi, and are not set by any standard. In SSPI, they're treated as separate objects, and perhaps I should've designed nsspi the same way. However, there's nothing stopping you from structuring the data you receive in the way that nsspi expects it. I can't help you if you don't know how your data is structured, however. I hazard a guess that it's specified in the gssapi spec.

@hambonewa
Copy link

I got the code modified and working. It appears the EncryptMessage is not thread safe. I had to add a lock to call EncryptMessage and to copy the adapter buffers after. And this is where each thread has its own unique context.

@antiduh
Copy link
Owner

antiduh commented Dec 15, 2018 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants