提出 #893207: open62541 Project open62541 1.5.5 Use After Free情報

タイトルopen62541 Project open62541 1.5.5 Use After Free
説明I identified a server-side heap use-after-free vulnerability in open62541 version 1.5.5 in the historical data handling path. The issue is reachable through a real OPC UA service request and is reproducible against the official example server examples/tutorial_server_historicaldata.c when historizing is enabled and the default in-memory history backend is used. The vulnerable execution path is not an artificial unit-test-only condition and does not depend on direct misuse of internal APIs. It is triggered through a crafted HistoryRead request carrying a syntactically valid but structurally extreme indexRange string. In my reproduction, the request uses an indexRange consisting of 101 dimensions such as "0,0,0,..." repeated 101 times. This string is accepted by the parser, enters the normal HistoryRead processing pipeline, corrupts object ownership semantics in the history backend, and causes a use-after-free during a later legitimate HistoryRead operation. The vulnerable path is: client HistoryReadRequest -> Service_HistoryRead in src/server/ua_services_attribute.c -> readRaw_service_default and getHistoryData_service_default in plugins/historydata/ua_history_database_default.c -> copyDataValues_backend_memory and UA_DataValue_backend_copyRange in plugins/historydata/ua_history_data_backend_memory.c. The root cause is a failure-path ownership bug combined with unchecked error propagation. More specifically, UA_DataValue_backend_copyRange performs a raw memcpy(dst, src, sizeof(UA_DataValue)) before attempting to copy only the requested range from src->value into dst->value. This is dangerous because UA_DataValue contains a UA_Variant, and the memcpy operation copies the internal Variant.data pointer, type pointer, length fields, and storage semantics from the backend-owned historical value into the response-side temporary object. At that point, the destination object is already aliased to the backend’s heap allocation. The helper then calls UA_Variant_copyRange. However, UA_Variant_copyRange rejects ranges whose dimensionsSize exceeds UA_MAX_ARRAY_DIMS, which is 100. Therefore, an indexRange with 101 dimensions causes the function to fail immediately. The critical flaw is that the backend-specific helper does not implement the same failure-safe semantics as the core library helper. The core UA_DataValue_copyRange in src/ua_types.c first copies the structure, then immediately reinitializes dst->value with UA_Variant_init(&dst->value), and clears the destination on failure. By contrast, the backend helper in ua_history_data_backend_memory.c does only the raw memcpy and directly calls UA_Variant_copyRange. If UA_Variant_copyRange fails, the destination object still contains the original backend pointer copied from src. No rollback is performed, and the object remains in a partially initialized yet apparently valid state. This already creates an ownership alias between the backend’s stored historical value and the response object. The second necessary condition is that the caller copyDataValues_backend_memory ignores the return value of UA_DataValue_backend_copyRange. The function continues processing, increments the output counter, and finally returns UA_STATUSCODE_GOOD. As a result, the higher layers in getHistoryData_service_default and readRaw_service_default treat the operation as successful and package the corrupted value into the normal HistoryRead response. This is visible from the attacker side: the first malformed request does not return an error but instead produces a successful service result and a successful per-result status. In my reproduction, the client output showed seeded=8, bad_index_range_len=201, dims=101, bad_service=Good, results=1, result0=Good. That behavior is important because it proves that the failure in the range-copy routine is swallowed instead of being propagated to the protocol layer. I validated the root cause with source-level GDB tracing and with an AddressSanitizer build of the official example server. During GDB analysis, I stopped in the backend helper and observed range.dimensionsSize = 101, src->value.data pointing to a live backend heap object, and dst->value.data initially NULL. After single-stepping over memcpy(dst, src, sizeof(UA_DataValue)), dst->value.data became equal to src->value.data, confirming that the destination object inherited the backend-owned pointer before any safe reinitialization took place. I then traced into UA_Variant_copyRange and confirmed that it returned failure at the dimension-limit check because the parsed numeric range contained 101 dimensions while the code allows at most 100. Returning to copyDataValues_backend_memory, I verified that the error code was not handled. The destination value array still contained the same backend pointer, the counter was advanced, and execution proceeded as though the HistoryRead had succeeded. The next stage of the bug is the erroneous free. Once the malformed HistoryRead request is processed, the server encodes and sends the response, then clears the response object in the normal message cleanup path. Because the corrupted response value still carries the backend’s Variant.data pointer, the generic cleanup sequence eventually reaches Variant_clear and UA_Array_delete, which free the heap buffer that actually belongs to the backend’s persistent historical storage. My GDB trace captured this exact moment: the freed pointer matched the pointer previously observed in the backend historical value. The stack during the free passed through UA_Array_delete, Variant_clear, DataValue_clear, UA_clear, and the response cleanup path in processMSG. This proves that the freed object is not an attacker-controlled transient allocation and not a benign temporary buffer; it is a backend-owned historical value that remains logically registered in the server after the response is sent. The final step is the actual use-after-free. After the first malicious HistoryRead has completed and freed the backend-owned memory indirectly through response cleanup, a second normal HistoryRead against the same historized node causes the server to read from that stale backend pointer. In my GDB trace, I observed the second request entering the ordinary data-copy path, where UA_Array_copy was invoked with the same address that had already been freed during the prior response cleanup. In the ASan build of the official tutorial_server_historicaldata example, this second request reliably triggered AddressSanitizer: heap-use-after-free. The observed stack was UA_Array_copy -> Variant_copy -> DataValue_copy -> UA_copy -> UA_DataValue_copy -> copyDataValues_backend_memory -> getHistoryData_service_default -> readRaw_service_default -> Service_HistoryRead. The free stack showed Variant_clear and response cleanup in processMSG, and the allocation stack showed the backend’s own historical storage path. Together, these traces establish a complete allocation -> aliasing -> free -> later read chain. The security impact is a remotely triggerable denial of service and memory-safety violation in the server process. A remote client that can invoke HistoryRead on a historized node can corrupt the lifetime invariants of backend-owned data and cause the server to dereference freed heap memory during a later normal read. In the official tutorial server configuration, anonymous access is enabled and the issue is reachable without authentication. In real deployments, exploitability depends on whether the target node is historized and whether the client has access to HistoryRead for that node, but the defect itself is in library code and not in the example application logic. I am not claiming code execution based on the current evidence. The demonstrated impact is a reliable heap use-after-free leading to process termination under ASan and a credible crash or undefined behavior in non-sanitized builds. In my assessment, the root cause is best described as an ownership mismatch on an error path in the default historical memory backend. The parser accepts an indexRange string that is syntactically valid, but the later range-copy operation rejects it due to an internal dimension cap. The backend helper has unsafe copy semantics because it clones UA_DataValue by raw memcpy and does not reset the embedded UA_Variant before attempting the range copy. The failure result is then ignored by the caller, so an aliased object is returned as if it were valid. Standard response cleanup subsequently frees memory still owned by the backend, leaving a dangling reference that is dereferenced during a subsequent HistoryRead. I discovered this issue while auditing open62541 1.5.5 and documented the affected path, the variable-level GDB evidence, and the reproducible ASan crash to support the report.
ソース⚠️ https://github.com/open62541/open62541/issues/8199
ユーザー
 VULL (UID 98817)
送信2026年07月16日 20:05 (1 月 ago)
モデレーション2026年08月30日 10:04 (1 month later)
ステータス承諾済み
VulDBエントリ397123 [open62541 迄 1.5.5 History Backend ua_history_data_backend_memory.c UA_DataValue_backend_copyRange メモリ破損]
ポイント20

Are you interested in using VulDB?

Download the whitepaper to learn more about our service!