CNA-VGAP-054: Destroying a caller-created GraphicsDevice while resources on it are still live is allowed by cna_graphics_device_destroy but untested

CNA snapshot 009d40f5  ·  Known Issues › Verification gaps  ·  source links pinned to 009d40f5

✓

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

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, &parameters, &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.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Maintainer workflow
Update the C API: traps