Provider development combines cryptographic computation with ordinary systems-programming obligations. The mathematical output can be correct while the surrounding code leaks memory, retains invalid pointers, or releases resources in the wrong order. The teaching provider is small enough that its complete ownership graph can be examined. This chapter explains that graph and connects it to the direct callback experiments.
The provider context contains three owned resources: a library context, a default-provider handle within it, and a fetched SHA-256 method. Initialization establishes them in that order. A later resource depends on earlier resources being available. Teardown reverses the important dependency direction: it releases the fetched method, unloads the backend handle, and then frees the library context. The allocation containing these fields is released last.
A single teardown path is also used for partial initialization failure. This reduces duplicated cleanup code, but it creates a review obligation: each cleanup operation must tolerate the states that can reach it. The prototype initializes the provider structure to zero so that fields not yet established are null. The successful experiments exercise the ordinary path; they do not systematically force every intermediate allocation or load to fail.
Every digest operation gets a separate inner EVP_MD_CTX. Its mutable message state is therefore not stored directly in the provider-wide structure. The digest context also holds a reference to the provider structure in the ordinary C sense of a pointer, not an independently counted application-level ownership claim. Correct lifetime therefore depends on the surrounding provider and method lifecycle as well as on the local allocation code.
The client follows a conservative release order: digest contexts are freed first, then the fetched method, then the provider handle, and finally the application library context. This explicit order makes the example easy to review. It should not be converted into an unsupported claim that every other sequence is safe or unsafe under every OpenSSL reference-counting condition. Such claims require the exact API contract and targeted tests. [5,6,8]
| Conceptual state | Transition | Responsibility |
|---|---|---|
| Allocated | newctx creates inner state. | Return a valid context or failure. |
| Initialized | init selects the cached SHA-256 method. | Establish a fresh operation. |
| Updating | update accepts a message fragment. | Preserve previous input and process new input. |
| Finalized | final returns 32 digest bytes. | Respect output capacity and reported length. |
| Duplicated branch | dupctx copies an intermediate operation. | Own independent mutable inner state. |
| Released | freectx frees the inner state and wrapper. | End ownership without reusing the pointer. |
The table is a conceptual model for valid use, not a complete specification of every possible invalid sequence. The prototype does not maintain an additional explicit enum for these states. It delegates much of the underlying state behavior to EVP. The tests therefore avoid asserting undocumented behavior for arbitrary sequences such as updating a released pointer or finalizing uninitialized memory. Those are not meaningful production use cases to support by accident.
The finalization callback receives an output pointer, a capacity, and a place to report the number of bytes written. The prototype requires at least 32 bytes. It reports zero before rejecting an insufficient buffer and does not call the backend when the guard fails. This arrangement has two testable consequences: caller storage should remain untouched during the rejected attempt, and the operation should remain available for a later correctly sized finalization.
The callback harness initializes a 64-byte output array with a sentinel value and varies the advertised capacity from zero to 33. For capacities below 32, it checks failure, a zero reported length, and unchanged sentinel bytes. It then retries with sufficient capacity and compares the result with the reference digest. For capacities 32 and 33, it checks success directly. Bytes beyond the 32-byte digest remain sentinel values in each successful case.
These observations provide concrete evidence about the boundary guard. They do not establish that arbitrary invalid pointers are safe to pass. A capacity value is a promise made by the caller about accessible storage; testing ordinary allocated arrays does not simulate malicious pointer values. The harness deliberately separates legitimate boundary conditions from undefined memory accesses that would not constitute a reasonable interface guarantee.
The baseline duplication test branches after processing one byte. Both branches then receive the same suffix and produce the same expected result. This proves that the implemented duplication path is reachable and preserves the tested prefix. It is a modest test: because both suffixes are equal, a stronger future case should feed different suffixes and verify two different expected digests. The paper retains that limitation rather than upgrading the claim beyond the implemented test.
A naive duplication that merely copied a pointer to the same inner EVP_MD_CTX would make both wrappers refer to shared mutable state and risk double release. The prototype instead allocates a new wrapper and uses EVP_MD_CTX_copy_ex. The function’s return value is checked, and a failed copy releases the newly allocated wrapper. This is an example of how a small amount of deliberate ownership code prevents a large class of confusing integration defects.
The direct tests include a null output pointer and a null output-length pointer in finalization. The implementation rejects them before producing a digest. It also tests reinitialization of an operation after these rejected calls. Parameter tests check a wrong type and an unknown name. Together these cases examine several boundaries that successful hashing alone would not exercise.
Memory-safety tooling remains an important next step. A sanitizer build could detect certain invalid accesses during the same tests. Leak analysis could inspect repeated load and unload cycles. Allocation-failure injection could examine rarely reached cleanup paths. None of these tools was run as part of the present results, so the evidence is described as boundary and ownership testing rather than as a complete memory-safety assessment.