ERROR_INSUFFICIENT_BUFFER and Concurrency

Many classic Windows APIs accept a pointer to a byte buffer and a pointer to an integer indicating the size of the buffer. If the buffer is large enough to hold the data returned from the API, the buffer is filled and the API returns S_OK. If the buffer supplied is not large enough to hold all of the data, the API instead returns ERROR_INSUFFICIENT_BUFFER, updating the supplied integer with the length of the buffer required. The client is expected to reallocate a new buffer of the specified size and call the API again with the new buffer and length.

For example, the InternetGetCookieEx function, used to query the WinINET networking stack for cookies for a given URL, is one such API. The GetExtendedTcpTable function, used to map sockets to processes, is another.

The advantage of APIs with this form is that you can call the API with a reasonably-sized stack buffer and avoid the cost of a heap allocation unless the stack buffer happens to be too small.

In the case of Internet Explorer and Edge, the document.cookie DOM API getter’s implementation first calls the InternetGetCookieEx API with a 1024 WCHAR buffer. If the buffer is big enough, the cookie string is then immediately returned to the page.

However, if ERROR_INSUFFICIENT_BUFFER is returned instead (and if the size needed is 10240 characters (MAX_COOKIE_LEN) or fewer), the API will allocate a new buffer on the heap and call the API again. If the API succeeds, the cookie string is returned to the page, otherwise if any error is returned, an empty string is returned to the page.

Wait. Do you see the problem here?

It’s tempting to conclude that the document.cookie API doesn’t need to be thread-safe–JavaScript that touches the DOM runs in one thread, the UI thread. But cookies are a form of data storage that is available across multiple threads and processes. For instance, subdownload network requests for the page’s resources can be manipulating the cookie store in parallel, and if I happen to have multiple tabs or windows open to the same site, they’ll be interacting with the same cookie jar.

So, consider following scenario: The document.cookie implementation calls InternetGetCookieEx but gets back ERROR_INSUFFICIENT_BUFFER with a required size of 1200 bytes. The implementation dutifully allocates a 1200 byte buffer, but before it gets the chance to call InternetGetCookieEx again, an image on the page sets a new 4 byte cookie which WinINET puts in the cookie jar. Now, when InternetGetCookieEx is called again, it again returns ERROR_INSUFFICIENT_BUFFER because the required buffer is now 1204 characters. Because document.cookie isn’t using any sort of loop-until-success, it returns an empty cookie string.

Now, this is all fast native code (C/C++), so surely this sort of thing is just theoretical… it can’t really happen on a fast computer, right?

Around ten years ago, I showed how you can use Meddler to easily generate a lot of web traffic for testing browsers. Meddler is a simple web server that has a simple GUI code editor slapped on the front (most developers would use node.js or Go for such tasks). I quickly threw together a tiny little MeddlerScript which exercises cookies by loading cookie-setting images in a loop and monitoring the document.cookie API to see if it ever returns an empty string.


import Meddler;
import System;
import System.Text;
import System.Net.Sockets;
import System.Windows.Forms;
// You can set options for this script using the format:
// ScriptOptions("StartURL" (where {$PORT} is autoreplaced by the Meddler port number), "Optional HTTPS Certificate Thumbprint", "Random # Seed")
// public ScriptOptions("https://localhost:{$PORT}/Test2", "fc ba fd cd 07 02 14 db a6 b7 ad 37 92 a9 65 0a 75 33 4f 9a", "1234")
class Handlers
{
static function OnConnection(oSession: Session)
{
try{
if (oSession.ReadRequest()){
var oHeaders: ResponseHeaders = new ResponseHeaders();
oHeaders.Status = "200 OK";
oHeaders["Connection"] = "close";
oHeaders["Cache-Control"] = "no-cache";
if (oSession.requestHeaders.Path.indexOf(".jpg")>-1){
oHeaders["Content-Type"] = "image/jpeg";
oHeaders["Set-Cookie"] = "C"+(Fuzz.NewInteger(1,7).ToString())+"="+Fuzz.NewString('a', Fuzz.NewInteger(128,256));
oSession.WriteString(oHeaders);
oSession.WriteBytes(Fuzz.NewJPG(Fuzz.NewInteger(100,999).ToString(), 80, 60));
}
else
{
oHeaders["Content-Type"] = "text/html";
oSession.WriteString(oHeaders);
oSession.WriteString("<!doctype html>\r\n<head>\r\n<title>Cookie Hammer</title>\r\n"
+ "<script>\r\n\r\nsetInterval(function(){\r\n let a=document.cookie;\r\n if (a.length < 1) { alert('Cookie was empty\\n' + document.cookie); }\r\n"
+ "document.getElementById(\"divLen\").innerText = new Date() + ' ' + (\"Cookie Length: \" + a.length.toString());\r\n\r\n }, 1);\r\n</script>\r\n"
);
oSession.WriteString("</head>\r\n<body>\r\n\r\n");
for (var i=0; i<64; i++)
oSession.WriteString("<img onload='this.src = \"CookieRandom.jpg?"+i.toString()+"=\" + Math.random();' src=\"CookieRandom.jpg?\" />\r\n");
oSession.WriteString("\r\n<div id=\"divLen\"></div>\r\n\r\n</body>\r\n</html>");
}
}
oSession.CloseSocket();
}
catch(e)
{
// MessageBox.Show("Script threw exception\n"+e, "OnConnection Failed");
MeddlerObject.Log.LogString("Script threw exception\n"+e);
}
}
// Optional method called on compile
static function Main(){
var today: Date = new Date();
MeddlerObject.StatusText = " Rules.js was loaded at: " + today;
}
}

view raw

CookieHammer.ms

hosted with ❤ by GitHub

Boy, does it ever. On my i7 machines, it usually only takes a few seconds to run into the buggy case where document.cookie returns an empty string.

Failure

I haven’t gone back to check the history, but I suspect this IE/Edge bug is at least fifteen years old.

After confirming this bug, it felt strangely familiar, as if I’d hit this landmine before. Then, as I was writing this post, I realized when… Back in 2011, I shared the C# code Fiddler uses for mapping a socket to a process. That code relies on the GetExtendedTcpTable API, which has the same reallocate-then-reinvoke design. Fortunately, I’d fixed the bug a few weeks later in Fiddler, but it looks like I never updated my blog post (sorry about that).

-Eric

PS: Unrelated, but one more pitfall to be aware of: InternetGetCookieExW has a truly bizarre shape, in that the lpdwSize argument is a pointer to a count of wide characters, but if ERROR_INSUFFICIENT_BUFFER is returned, the size argument is set to the count of bytes required.

PPS: As of Windows 10 RS3, Edge (and IE) support 180 cookies per domain to match Chrome, but the network stack will skip setting or sending individual cookies with a value over 5120 bytes.

Published by ericlaw

Impatient optimist. Dad. Author/speaker. Created Fiddler & SlickRun. PM @ Microsoft 2001-2012, and 2018-2022, working on Office, IE, and Edge. Now a SWE on Microsoft Defender Web Protection. My words are my own, I do not speak for any other entity.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: