Since OWL 5, at least, TWindow::EvCtlColor has returned a handle to a TBrush created locally within the function scope. This is usually an awful programming mistake, but here it is intended to work.
::SetBkColor(hDC, bkgndColor);
return TBrush{bkgndColor}; // HBRUSH will stay in cache.
As the code comment points out, this awful code relies on the fact that TBrush is implemented as having shared ownership of the underlying handle, which is also entered into a 16-entry cache of solid brushes by the TBrush constructor, if applicable and space allows. However, relying on the cache is unsafe, since it may run out of free space.
Having briefly reviewed the cache implementation in "brush.cpp (sourceforge.net)", it looks like it is meant to have support for a replacement policy. A replacement policy, assuring room for the latest entry, would ensure that the handle returned by EvCtlColor at least remained alive throughout the processing of the message. However, as I understand it, there is no replacement implemented, contrary to what the code comment claims. The cache will simply refuse another entry, if full. If that happens for the brush created in EvCtlColor, it will be destroyed at the end of the scope, and the handle will be dangling.
//
// Add a new entry to the front, pushing the last old one off the end.
//
bool
TBrushCache::AddRef(HANDLE handle, COLORREF color)
{
// ...
bool retval = true;
if (Entries[Size-1].Handle){ // ...last slot occupied?...
int i = Size-1;
for(; i >= 0; i--){ // ...search for free slot...
// if RefCnt == 0 no one use this
if(!Entries[i].RefCnt){
/// ...use this slot...
break;
}
}
if(i < 0) // free node not found
retval = false;
}
else{
/// ...last slot free, so use it...
}
// ...
}
I strongly suspect there is no one left active on the OWLNext project that properly understands the TBrush caching, as well as the overall GDI object sharing and reference counting scheme, on a detailed level, deep enough to properly trust and maintain it. So in Owlet, I am in the process of replacing it all with much simpler and maintainable code.
This particular issue was fixed in Owlet in [r5475] by adding a private member TWindow::BkgndBrush to hold the returned brush.
Commit: [r5475]
Feature Requests: #178
News: 2026/08/owlet---gdi-overhaul-and-new-shared-pointer-semantics
Claude Code:
Merged into trunk in [r8974] adapted from the Owlet fix [r5475]. Trunk uses
std::unique_ptr<TBrush> BkgndBrush, matching the existingTWindow::Fontmember, rather thanstd::optional. The layout of TWindow has changed, so dependent code must be rebuilt.A correction to the description:
TBrushCache::AddRefdoes have a replacement policy. The code quoted above leaves out the body of the search loop. When the cache is full,AddRefevicts the last entry withRefCnt == 0and deletes its handle. The cache refuses a new brush only if all 16 entries are held by liveTBrushobjects.The bug is still real, and it shows up in two ways:
TBrushinEvCtlColorthen owns its handle and deletes it on return, so the control is given a deleted brush.RefCnt == 0once the temporary is destroyed. The next uncached brush can evict it and delete the handle, and GDI may then reuse the handle value for a different object.I checked this with a headless test that sends
WM_CTLCOLORSTATICto aTWindowwith a background colour set, and checks the returned handle withGetObjectType/GetObject.GetObjectTypereturned 0). In case 2 the handle had been reused by a brush of a different colour.TColor::NoneandTColor::Transparentpaths are unchanged.Related
Commit: [r5475]
Commit: [r8974]