CNA-VGAP-054: Destroying a caller-created GraphicsDevice while resources on it are still live is allowed by cna_graphics_device_destroy but untested
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
graphics_device.h says resources on a caller-created device are released with it and the destroy route checks no child count, but every C test destroys the resources first, so what later calls on those resource handles do rests on reading.
- Identifier
CNA-VGAP-054- Category
- Verification gap
- Subsystem
- C API & bindings
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- cna_graphics_device_create / cna_graphics_device_destroy (CNA/C/graphics_device.h) and the resource handles created on a caller-created device
Expected behaviour
A sequence the header explicitly permits (destroying a caller-created device while resources made on it still exist) has a test that pins what happens to those resources and to their still-registered handles, and the header says whether each handle must still be destroyed.
Actual behaviour at TARGET
cna_graphics_device_destroy resolves the OwnedGraphicsDevice, calls GraphicsDevice::Dispose() and releases the handle; it counts no children, by design (AddOwnedGraphicsResourceFor counts only game-owned resources so a standalone device cannot block cna_game_destroy). By reading, the C++ layer makes the order safe: GraphicsDevice::Dispose(bool) disposes every tracked resource, ~GraphicsDevice resets resourceDeviceLifetime_, a resource's later Dispose/destructor skips the device once that token has expired, and data routes that validate disposal (for example VertexBuffer's SetData validation) throw ObjectDisposedException, which the C exception barrier turns into a result. Handles are generation-checked, so the released device handle cannot be confused with a later one; cna_graphics_resource_get_graphics_device is refused for such a resource whether or not its device still exists (a separate entry). The resource handles stay registered until the caller destroys each one; the header's “released with it” does not say whether that call is still required. None of this is exercised: OwnedGraphicsDeviceSmoke.c destroys its texture before the device, CnbToolingSmoke.c its content manager first, and GameSecondaryGraphicsDeviceContextSmoke.c leaves no resource on the device it destroys.
Source locations
modules/c-api/src/CnaCApiDisplay.cpp— cna_graphics_device_destroy and cna_graphics_device_createmodules/c-api/include/CNA/C/graphics_device.h— Doxygen of cna_graphics_device_create: resources are released with the devicemodules/c-api/src/CnaCApiRuntimeDetail.hpp— OwnedGraphicsDevice, BorrowedGraphicsDevicemodules/c-api/src/CnaCApiRuntime.cpp— GetBorrowedGraphicsDevice, AddOwnedGraphicsResourceFormodules/graphics/src/Xna/GraphicsDevice.cpp— GraphicsDevice::Dispose(bool) disposes resources_; ~GraphicsDevice resets resourceDeviceLifetime_modules/graphics/src/Xna/GraphicsResource.cpp— GraphicsResource::Dispose(bool) checks graphicsDeviceLifetime_modules/c-api/tests/pure_c/OwnedGraphicsDeviceSmoke.c— destroys the texture before the device
Evidence
Checked by reading the files above at 009d40f5; the C library was not built and nothing was run. The resource routes sampled were the index-buffer family and cna_graphics_resource_get_graphics_device; not every resource family was traced for a path that reaches the device through a raw pointer after disposal.
Independently observed as a separate finding (merged): graphics_device.h says resources on a caller-created device are released with it and the destroy route counts no children, but every pure-C test destroys the resources first, so the state of their still-registered handles rests on reading.
Focused reproduction
Illustrative only, not compiled: the test that does not exist.
CNA_Handle device = CNA_INVALID_HANDLE, texture = CNA_INVALID_HANDLE;
cna_graphics_device_create(0U, CNA_GRAPHICS_PROFILE_REACH, ¶meters, &device);
cna_texture2d_create(device, &texture_info, &texture);
cna_graphics_device_destroy(device); /* permitted: no child count */
CNA_Bool disposed = CNA_FALSE;
cna_graphics_resource_get_is_disposed(texture, &disposed); /* expected: CNA_TRUE */
cna_texture2d_destroy(texture); /* expected: success, no use-after-free */
Current tests
OwnedGraphicsDeviceSmoke.c, GameSecondaryGraphicsDeviceContextSmoke.c and CnbToolingSmoke.c all destroy children first. The C tests are not run by any CI workflow.
Regression test
A pure-C test that creates a texture, an index buffer and a sprite batch on a caller-created device, destroys the device first, then expects get_is_disposed to answer true, a data route to return a documented result, and each destroy to succeed, run under AddressSanitizer.
Blast radius
C and binding callers of cna_graphics_device_create; resources on a game's device are unaffected, because cna_game_destroy is gated by their count.
Workaround
Destroy every resource created on a caller-created device before destroying the device, as the existing tests do.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- C API: the model
- Maintainer workflow
- Update the C API: traps
- Known issues
- Verification gap index