Skip to content

Commit d5dcc83

Browse files
JakeStevensfacebook-github-bot
authored andcommitted
Fix XNN PreLU use after free (#21480)
Summary: Most XNNPACK parameter / constant data is packed and XNNPACK takes ownership of it. This means that we can free the buffer after load. However, PreLU doesn't pack and thus expects the buffer to stay alive. There is a reproducible segfault when running PreLU on ET XNNPACK when running from a file instead of in-memory buffer data. This change fixes that by only freeing buffers for ops we know take ownership of the data. Practically speaking this should be a no-op for most cases like linear and conv, but it plays it safe for other constant tensors. Fixes #17559. Differential Revision: D114141346
1 parent b26b9ac commit d5dcc83

4 files changed

Lines changed: 136 additions & 6 deletions

File tree

backends/xnnpack/runtime/XNNCompiler.cpp

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <cinttypes>
1616
#include <string>
1717
#include <unordered_map>
18+
#include <unordered_set>
1819
#include <vector>
1920

2021
#pragma clang diagnostic ignored "-Wmissing-prototypes"
@@ -2093,19 +2094,27 @@ ET_NODISCARD Error XNNCompiler::compileModel(
20932094
// Invalid ids do not need to be remapped
20942095
remapped_ids.emplace(XNN_INVALID_VALUE_ID, XNN_INVALID_VALUE_ID);
20952096

2096-
// If weight cache is not on we hold onto all the unpacked buffers
2097-
// and we free them at the end
2097+
// Buffers loaded from the named data map. After xnn_create_runtime,
2098+
// buffers consumed by packing operators are freed; the rest are moved
2099+
// into the executor to keep them alive for non-packing operators.
20982100
std::vector<FreeableBuffer> unpacked_buffers;
20992101

2102+
// Maps xvalue index to unpacked_buffers index for values whose data was
2103+
// loaded from the named data map. Used to selectively retain buffers that
2104+
// are still referenced at runtime (non-packing operators).
2105+
std::unordered_map<uint32_t, size_t> named_data_buffer_map;
2106+
21002107
// External Ids for inputs and outputs
21012108
std::vector<uint32_t> input_ids;
21022109
std::vector<uint32_t> output_ids;
21032110
Error err = Error::Ok;
2104-
for (auto value : *flatbuffer_graph->xvalues()) {
2111+
auto xvalues = flatbuffer_graph->xvalues();
2112+
for (uint32_t i = 0; i < xvalues->size(); i++) {
2113+
size_t prev_buffers = unpacked_buffers.size();
21052114
err = defineTensor(
21062115
subgraph.get(),
21072116
remapped_ids,
2108-
value,
2117+
xvalues->Get(i),
21092118
flatbuffer_graph,
21102119
constant_data,
21112120
constant_data_size,
@@ -2120,6 +2129,10 @@ ET_NODISCARD Error XNNCompiler::compileModel(
21202129
if (err != Error::Ok) {
21212130
return err;
21222131
}
2132+
2133+
if (unpacked_buffers.size() > prev_buffers) {
2134+
named_data_buffer_map[i] = prev_buffers;
2135+
}
21232136
}
21242137

21252138
for (auto node : *flatbuffer_graph->xnodes()) {
@@ -2174,8 +2187,52 @@ ET_NODISCARD Error XNNCompiler::compileModel(
21742187
"Failed to finalize weights cache after creating the xnn runtime");
21752188
packed_weights_names = std::move(packed_weights_names_result.get());
21762189
} else {
2177-
for (auto& buffer : unpacked_buffers) {
2178-
buffer.Free();
2190+
// Operators like convolution and fully-connected pack weights during load,
2191+
// so those buffers can be freed. Other operators (PreLU) retain raw
2192+
// pointers to the original constant data, so those buffers need to remain
2193+
// alive.
2194+
if (!named_data_buffer_map.empty()) {
2195+
std::unordered_set<uint32_t> packed_value_indices;
2196+
for (auto node : *flatbuffer_graph->xnodes()) {
2197+
auto type = node->xnode_union_type();
2198+
switch (type) {
2199+
case fb_xnnpack::XNodeUnion::XNNFullyConnected: {
2200+
auto n = node->xnode_union_as_XNNFullyConnected();
2201+
packed_value_indices.insert(n->filter_id());
2202+
packed_value_indices.insert(n->bias_id());
2203+
break;
2204+
}
2205+
case fb_xnnpack::XNodeUnion::XNNConv2d: {
2206+
auto n = node->xnode_union_as_XNNConv2d();
2207+
packed_value_indices.insert(n->filter_id());
2208+
packed_value_indices.insert(n->bias_id());
2209+
break;
2210+
}
2211+
case fb_xnnpack::XNodeUnion::XNNDepthwiseConv2d: {
2212+
auto n = node->xnode_union_as_XNNDepthwiseConv2d();
2213+
packed_value_indices.insert(n->filter_id());
2214+
packed_value_indices.insert(n->bias_id());
2215+
break;
2216+
}
2217+
case fb_xnnpack::XNodeUnion::XNNConvTranspose2d: {
2218+
auto n = node->xnode_union_as_XNNConvTranspose2d();
2219+
packed_value_indices.insert(n->filter_id());
2220+
packed_value_indices.insert(n->bias_id());
2221+
break;
2222+
}
2223+
default:
2224+
break;
2225+
}
2226+
}
2227+
2228+
for (auto& [value_idx, buffer_idx] : named_data_buffer_map) {
2229+
if (packed_value_indices.count(value_idx)) {
2230+
unpacked_buffers[buffer_idx].Free();
2231+
} else {
2232+
executor->unpacked_buffers_.push_back(
2233+
std::move(unpacked_buffers[buffer_idx]));
2234+
}
2235+
}
21792236
}
21802237
}
21812238

backends/xnnpack/runtime/XNNExecutor.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <executorch/runtime/backend/interface.h>
1515
#include <executorch/runtime/core/error.h>
1616
#include <executorch/runtime/core/exec_aten/util/tensor_util.h>
17+
#include <executorch/runtime/core/freeable_buffer.h>
1718

1819
#include <xnnpack.h>
1920
#include <atomic>
@@ -30,6 +31,10 @@ class XNNWeightsCache;
3031

3132
class XNNExecutor {
3233
private:
34+
// For XNN constant data that isn't packed (PreLU weights, for example),
35+
// we need to hold onto the buffers to keep them alive.
36+
std::vector<executorch::runtime::FreeableBuffer> unpacked_buffers_;
37+
3338
std::unique_ptr<xnn_runtime, decltype(&xnn_delete_runtime)> runtime_{
3439
nullptr,
3540
&xnn_delete_runtime};

backends/xnnpack/test/ops/test_prelu.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import tempfile
78
import unittest
9+
from pathlib import Path
810

911
import torch
12+
from executorch.backends.test.harness.stages import StageType
13+
from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner
1014
from executorch.backends.xnnpack.test.tester import Tester
15+
from executorch.exir import to_edge_transform_and_lower
16+
from executorch.exir.capture._config import ExecutorchBackendConfig
17+
from executorch.runtime import Runtime, Verification
1118

1219

1320
class TestPrelu(unittest.TestCase):
@@ -23,6 +30,14 @@ def forward(self, x):
2330
a = self.prelu(x)
2431
return a
2532

33+
class ConstWPrelu(torch.nn.Module):
34+
def __init__(self):
35+
super().__init__()
36+
self.register_buffer("w", torch.ones(3, dtype=torch.float32))
37+
38+
def forward(self, x):
39+
return torch.ops.aten.prelu.default(x, self.w)
40+
2641
def _test_prelu(self, module, inputs):
2742
(
2843
Tester(module, inputs)
@@ -38,6 +53,18 @@ def _test_prelu(self, module, inputs):
3853
.run_method_and_compare_outputs()
3954
)
4055

56+
def _load_and_compare_from_file(self, write_program, inputs, expected):
57+
with tempfile.TemporaryDirectory() as temp_dir:
58+
pte_path = Path(temp_dir) / "prelu.pte"
59+
with pte_path.open("wb") as f:
60+
write_program(f)
61+
62+
rt = Runtime.get()
63+
program = rt.load_program(pte_path, verification=Verification.Minimal)
64+
method = program.load_method("forward")
65+
actual = method.execute(inputs)[0]
66+
self.assertTrue(torch.allclose(expected, actual, atol=1e-5))
67+
4168
@unittest.skip("XNNPACK Expects FP16 inputs but FP32 weights")
4269
def _test_fp16_prelu(self):
4370
module = self.PReLU().to(torch.float16)
@@ -48,3 +75,43 @@ def test_fp32_prelu(self):
4875
module = self.PReLU()
4976
inputs = (torch.randn(1, 5, 3, 2),)
5077
self._test_prelu(module, inputs)
78+
79+
def test_fp32_prelu_file_load(self):
80+
"""
81+
Make sure that PreLU doesn't free its weight buffer after load. It's a weird
82+
op that doesn't copy or pack its data, so we need to hold onto the buffer.
83+
Run specifically from a file to exercise the path.
84+
"""
85+
module = self.PReLU()
86+
module.eval()
87+
x = torch.randn(1, 5, 3, 2)
88+
expected = module(x)
89+
90+
tester = Tester(module, (x,))
91+
tester.export()
92+
tester.to_edge_transform_and_lower()
93+
tester.check_count({"torch.ops.higher_order.executorch_call_delegate": 1})
94+
tester.to_executorch()
95+
tester.serialize()
96+
97+
buf = tester.stages[StageType.SERIALIZE].artifact
98+
self._load_and_compare_from_file(lambda f: f.write(buf), (x,), expected)
99+
100+
def test_fp32_prelu_constant_weight_empty_decompositions_file_load(self):
101+
module = self.ConstWPrelu().eval()
102+
x = torch.randn(2, 3, 3, 3, device="cpu", dtype=torch.float32)
103+
expected = module(x)
104+
105+
exported = torch.export.export(module, args=(x,), strict=True)
106+
exported = exported.run_decompositions({})
107+
108+
edge_pm = to_edge_transform_and_lower(
109+
exported,
110+
partitioner=[XnnpackPartitioner()],
111+
compile_config=None,
112+
)
113+
et_pm = edge_pm.to_executorch(
114+
config=ExecutorchBackendConfig(extract_delegate_segments=True)
115+
)
116+
117+
self._load_and_compare_from_file(et_pm.write_to_file, (x,), expected)

backends/xnnpack/xnnpack_preprocess.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def preprocess(
140140
passes.append(ConvertToLinearPass)
141141

142142
passes = passes if len(passes) > 0 else None
143+
143144
# XNNPACK Delegate Specific Passes
144145
ep = XNNPACKPassManager(ep, passes=passes).transform()
145146
graph_module = ep.graph_module

0 commit comments

Comments
 (0)