Skip to content

CAGLTexture: heap-allocate the pixel buffer in -_writeToPNG: (stack overflow)#11

Open
DTW-Thalion wants to merge 3 commits into
gnustep:masterfrom
DTW-Thalion:fix/cagltexture-vla
Open

CAGLTexture: heap-allocate the pixel buffer in -_writeToPNG: (stack overflow)#11
DTW-Thalion wants to merge 3 commits into
gnustep:masterfrom
DTW-Thalion:fix/cagltexture-vla

Conversation

@DTW-Thalion

Copy link
Copy Markdown
Contributor

Summary

-_writeToPNG: reads the whole texture into a stack VLA:

char pixels[[self width]*[self height]*4];

For a 1024×1024 texture that is 4 MB on the stack — a stack overflow.

Fix

Allocate the buffer on the heap (with a NULL check) and free it at the end.
The buffer is used as the backing store for the bitmap context until
CGBitmapContextCreateImage snapshots it, so it is freed only after the image
has been created and written.

Validation

Built libQuartzCore (clang, gnustep-2.0, against libs-corebase + libs-opal +
libs-gui); CAGLTexture.m compiles cleanly.

-_writeToPNG: read the texture into a stack VLA,
char pixels[[self width]*[self height]*4].  For a 1024x1024 texture that
is 4 MB on the stack -- a stack overflow.  Allocate it on the heap and
free it.
@DTW-Thalion DTW-Thalion requested a review from ivucica as a code owner June 28, 2026 21:55

@ivucica ivucica left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DTW-Thalion These three PRs are an interesting set of patches. Thank you!

Are you working on a project that will (finally) use the library? I'd be curious to learn more about it. I had reasons why I wanted a (~correctly implemented and opensource) version of CA, but I never got far enough to get that beyond the idea stage.

I'm also curious if you have any larger improvements in the pipeline that might warrant a design discussion? Example: I never got around to working on CATransition, and shadow radiuses are unlikely to be correct, and I don't think there are background or contents filters implemented in any way (likely an incompatible hack makes sense rather than full interface compatibility for those filters, as IIRC they're done via CI filters which I don't think GNUstep has an implementation of).


I have only one real request to change, just move free() earlier in the function so anyone adding an early return has a smaller chance of creating a leak. (Even if it is a debug method.)

It's also optional; if you'd prefer not to do it, I won't block this.

Comment thread Source/GLHelpers/CAGLTexture.m Outdated
CGImageRelease(image);

[data writeToFile:path atomically:YES];
free(pixels);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please free this as soon as it is no longer necessary, after line 264.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change made based on a bit of extra digging.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d46b153 is addressing this but can't you do it straight after line 264? Immediately after CGBitmapContextCreate? Again, we can merge as is (I'm needlessly nitpicky) but doing it asap = less chance of leaks. (Less important in this debug function, but still nice)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If it causes a crash due to CGBitmapContextCreate not actually copying the pixels array, that's ok, then existing state is correct. I can merge this anyway.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is what I am seeing - it needs to be after the CGImageRelease(image);

- (void) _writeToPNG:(NSString*)path
{
char pixels[[self width]*[self height]*4];
char *pixels = malloc([self width]*[self height]*4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice and correct improvement.

But, please note this is an internal debug method, and should not really be used in any production code. I hope you are not seeing this anywhere in particular except while developing. This method is not part of the public API (in fact, this entire class is not part of it, and we should probably rename that and a bunch of other things). The correct way to save the image is not to query OpenGL for the pixels; it is to either use CARenderer and save the whole scene, or to get contents via CGBitmapContextRef and save that (luckily, we don't yet have anything other than CGBitmapContextRef as source of contents). Downside of using contents of a CALayer is, of course, you don't get the exact pixels fed into pixelshaders, so it's not as easy to get intermediary GL textures if you ever need that.

And a small remark: By itself, 8MB default stack size on Linux and macOS should typically be fine for your example of 4MB. Apparently Windows has 1MB limit; if you're porting this to Windows, this would be very exciting to me, I think I never built a CA-using binary on Windows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something our folks are trying to decide internally. For our needs we have a lot of rendering happening - and we're trying to figure out how we want our researcher workstations to operate. We may end up using Unreal Engine instead, but we wanted to play with what exists already in the GNUStep world.

Move free(pixels) to immediately after CGImageRelease(image), before the
file is written, so a future early return added around the file write
cannot leak the buffer (per review feedback).

The buffer cannot be freed any earlier: CGBitmapContextCreate uses it in
place as the bitmap backing store and CGBitmapContextCreateImage returns
an image that references it through a data provider (no snapshot), so it
must stay live until CGImageRelease.
@DTW-Thalion

Copy link
Copy Markdown
Contributor Author

@ivucica to answer your question on our project: we are building a large computational biology engine with modelling of cells and proteins (https://thalion.global/programs/computational-modelling).

In April we made a decision to consider Objective-C as the language of choice for our server architecture, and that precipitated doing a review of how solid GNUStep would be in a high-availability, multi-node, multi-threaded environment. So we did a first pass to scope the various repos using Claude 4.6 (which is how PR#6/7 happened). With that in hand, we took a copy of the repos and had our dev-team start building test harnesses for our usage model (multiple-GPUs, multi-node, etc.) and did a lot of load testing in various configurations. As we did that we would encounter various issues, and they would be reported internally.

When we find an issue, we fix it on our side, but to offer it back to the community, we have to unbolt it from our codebase, and when I have time I have been picking away at packaging up each as a seperate PR in the form that RFM asked for. So that is what you are seeing now.

I had offered a number of changes directly from the Claude review, but noted the pushback on anything LLM direct, so that's fair, I am just much slower than LLM and work on them as I have time and build out the PRs by hand as necessary.

Add Tests/headless, a display-free test that creates a surfaceless EGL
desktop-GL context (Mesa software rendering) and exercises
CAGLTexture -_writeToPNG: directly, obtaining the class with objc_getClass:

  - a texture round-trips to a valid PNG of the expected dimensions;
  - writing a large texture does not overflow the stack -- the previous
    implementation read the image into a stack VLA (char pixels[w*h*4]).

The large-texture case runs on a thread with a deliberately small stack so
the regression is deterministic regardless of the caller's stack limit.

The test only builds when EGL is available (pkg-config egl) and skips at
run time if no GL context can be created, so it is inert where headless GL
is unavailable.  Run it with "make check" in Tests/headless.
@DTW-Thalion

Copy link
Copy Markdown
Contributor Author

Added Tests/headless/ — a display-free regression test for this fix.

It creates a surfaceless EGL context backed by Mesa's software rasteriser (llvmpipe), obtains CAGLTexture via objc_getClass, and exercises -_writeToPNG: directly: it checks that a texture round-trips to a valid PNG of the right dimensions, and that writing a large texture no longer overflows the stack (run on a thread with a small stack so the pre-fix VLA fails deterministically). It needs no X server or GPU.

The target is gated on EGL: it only builds when pkg-config egl succeeds and skips at run time if no GL context can be created, so it's inert where headless GL isn't available. make check in Tests/headless drives it.

The one CI cost is the dependency: to actually run it the runner needs EGL plus a Mesa software-GL driver. On Ubuntu that's libegl-dev (and libgl-dev) at build time and libgl1-mesa-dri + libegl-mesa0 at run time; it then renders on llvmpipe. Where those aren't installed the test simply skips rather than failing the build.

Since you mentioned headless GL testing never got past the idea stage — this approach (surfaceless EGL + objc_getClass, no windowing) generalises to FBO render-and-readback for parts of CARenderer too, if that would be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants