1cf48211cSNick Fitzgerald //! Utilities for testing and fuzzing out-of-memory handling.
2cf48211cSNick Fitzgerald //!
3cf48211cSNick Fitzgerald //! Inspired by SpiderMonkey's `oomTest()` helper:
4cf48211cSNick Fitzgerald //! https://firefox-source-docs.mozilla.org/js/hacking_tips.html#how-to-debug-oomtest-failures
5cf48211cSNick Fitzgerald
6cf48211cSNick Fitzgerald use backtrace::Backtrace;
7*5c68fe60SNick Fitzgerald use std::{
8*5c68fe60SNick Fitzgerald alloc::GlobalAlloc,
9*5c68fe60SNick Fitzgerald cell::Cell,
10*5c68fe60SNick Fitzgerald mem,
11*5c68fe60SNick Fitzgerald pin::Pin,
12*5c68fe60SNick Fitzgerald ptr,
13*5c68fe60SNick Fitzgerald task::{Context, Poll},
14*5c68fe60SNick Fitzgerald time,
15*5c68fe60SNick Fitzgerald };
16*5c68fe60SNick Fitzgerald use wasmtime_core::error::{OutOfMemory, Result, bail};
17cf48211cSNick Fitzgerald
18cf48211cSNick Fitzgerald /// An allocator for use with `OomTest`.
19cf48211cSNick Fitzgerald #[non_exhaustive]
20cf48211cSNick Fitzgerald pub struct OomTestAllocator;
21cf48211cSNick Fitzgerald
22cf48211cSNick Fitzgerald impl OomTestAllocator {
23cf48211cSNick Fitzgerald /// Create a new OOM test allocator.
new() -> Self24cf48211cSNick Fitzgerald pub const fn new() -> Self {
25cf48211cSNick Fitzgerald OomTestAllocator
26cf48211cSNick Fitzgerald }
27cf48211cSNick Fitzgerald }
28cf48211cSNick Fitzgerald
29cf48211cSNick Fitzgerald #[derive(Clone, Debug, Default, PartialEq, Eq)]
30cf48211cSNick Fitzgerald enum OomState {
31cf48211cSNick Fitzgerald /// We are in code that is not part of an OOM test.
32cf48211cSNick Fitzgerald #[default]
33cf48211cSNick Fitzgerald OutsideOomTest,
34cf48211cSNick Fitzgerald
35cf48211cSNick Fitzgerald /// We are inside an OOM test and should inject an OOM when the counter
36cf48211cSNick Fitzgerald /// reaches zero.
37755979ddSNick Fitzgerald OomOnAlloc {
38755979ddSNick Fitzgerald counter: u32,
39755979ddSNick Fitzgerald allow_alloc_after: bool,
40755979ddSNick Fitzgerald },
41cf48211cSNick Fitzgerald
42cf48211cSNick Fitzgerald /// We are inside an OOM test and we already injected an OOM.
43755979ddSNick Fitzgerald DidOom { allow_alloc: bool },
44cf48211cSNick Fitzgerald }
45cf48211cSNick Fitzgerald
46cf48211cSNick Fitzgerald thread_local! {
47cf48211cSNick Fitzgerald static OOM_STATE: Cell<OomState> = const { Cell::new(OomState::OutsideOomTest) };
48cf48211cSNick Fitzgerald }
49cf48211cSNick Fitzgerald
50cf48211cSNick Fitzgerald /// Set the new OOM state, returning the old state.
set_oom_state(state: OomState) -> OomState51cf48211cSNick Fitzgerald fn set_oom_state(state: OomState) -> OomState {
52cf48211cSNick Fitzgerald OOM_STATE.with(|s| s.replace(state))
53cf48211cSNick Fitzgerald }
54cf48211cSNick Fitzgerald
55cf48211cSNick Fitzgerald /// RAII helper to set the OOM state within a block of code and reset it upon
56cf48211cSNick Fitzgerald /// exiting that block (even if exiting via panic unwinding).
57*5c68fe60SNick Fitzgerald struct ScopedOomState<'a> {
58*5c68fe60SNick Fitzgerald state: &'a mut OomState,
59cf48211cSNick Fitzgerald }
60cf48211cSNick Fitzgerald
61*5c68fe60SNick Fitzgerald impl<'a> ScopedOomState<'a> {
62*5c68fe60SNick Fitzgerald /// The OOM state will be initialized to `*state` on construction, and then
63*5c68fe60SNick Fitzgerald /// `state` will be updated to be the old OOM state that existed when the
64*5c68fe60SNick Fitzgerald /// original state is replaced on this type's drop.
new(state: &'a mut OomState) -> Self65*5c68fe60SNick Fitzgerald fn new(state: &'a mut OomState) -> Self {
66*5c68fe60SNick Fitzgerald *state = set_oom_state(mem::take(state));
67*5c68fe60SNick Fitzgerald ScopedOomState { state }
68cf48211cSNick Fitzgerald }
69cf48211cSNick Fitzgerald }
70cf48211cSNick Fitzgerald
71*5c68fe60SNick Fitzgerald impl Drop for ScopedOomState<'_> {
drop(&mut self)72cf48211cSNick Fitzgerald fn drop(&mut self) {
73*5c68fe60SNick Fitzgerald *self.state = set_oom_state(mem::take(self.state));
74cf48211cSNick Fitzgerald }
75cf48211cSNick Fitzgerald }
76cf48211cSNick Fitzgerald
77cf48211cSNick Fitzgerald unsafe impl GlobalAlloc for OomTestAllocator {
alloc(&self, layout: std::alloc::Layout) -> *mut u878cf48211cSNick Fitzgerald unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
79cf48211cSNick Fitzgerald let old_state = set_oom_state(OomState::OutsideOomTest);
80cf48211cSNick Fitzgerald
81cf48211cSNick Fitzgerald let new_state;
82cf48211cSNick Fitzgerald let ptr;
83b236888cSNick Fitzgerald
84b236888cSNick Fitzgerald 'outside_oom_test: {
85cf48211cSNick Fitzgerald // NB: It's okay to log/backtrace/etc... in this block because the
86cf48211cSNick Fitzgerald // current state is `OutsideOomTest`, so any re-entrant allocations
87cf48211cSNick Fitzgerald // will be passed through to the system allocator.
88cf48211cSNick Fitzgerald
89b236888cSNick Fitzgerald // Don't panic on allocation-after-OOM attempts if we
90b236888cSNick Fitzgerald // are already in the middle of panicking. That will
91b236888cSNick Fitzgerald // cause an abort and we won't get as good of an error
92b236888cSNick Fitzgerald // message for the original panic, which is most likely
93b236888cSNick Fitzgerald // some kind of test failure.
94b236888cSNick Fitzgerald if old_state == OomState::OutsideOomTest || std::thread::panicking() {
95b236888cSNick Fitzgerald new_state = old_state;
96cf48211cSNick Fitzgerald ptr = unsafe { std::alloc::System.alloc(layout) };
97b236888cSNick Fitzgerald break 'outside_oom_test;
98cf48211cSNick Fitzgerald }
99071704b4SNick Fitzgerald
100b236888cSNick Fitzgerald let bt = Backtrace::new();
101b236888cSNick Fitzgerald let bt = format!("{bt:?}");
102b236888cSNick Fitzgerald
103b236888cSNick Fitzgerald // XXX: `env_logger` internally buffers writes in a `Vec` which
104b236888cSNick Fitzgerald // means our OOM tests might sporadically fail when you enable
105b236888cSNick Fitzgerald // logging to debug stuff, so simply let the allocation through if
106b236888cSNick Fitzgerald // `env_logger` is on the stack.
107b236888cSNick Fitzgerald if bt.contains("env_logger") {
108b236888cSNick Fitzgerald new_state = old_state;
109b236888cSNick Fitzgerald ptr = unsafe { std::alloc::System.alloc(layout) };
110b236888cSNick Fitzgerald break 'outside_oom_test;
111b236888cSNick Fitzgerald }
112b236888cSNick Fitzgerald
113b236888cSNick Fitzgerald match old_state {
114b236888cSNick Fitzgerald OomState::OutsideOomTest => unreachable!("handled above"),
115b236888cSNick Fitzgerald
116755979ddSNick Fitzgerald OomState::OomOnAlloc {
117755979ddSNick Fitzgerald counter: 0,
118755979ddSNick Fitzgerald allow_alloc_after,
119755979ddSNick Fitzgerald } => {
120cf48211cSNick Fitzgerald log::trace!(
121b236888cSNick Fitzgerald "injecting OOM for allocation: {layout:?}\nAllocation backtrace:\n{bt}"
122cf48211cSNick Fitzgerald );
123755979ddSNick Fitzgerald new_state = OomState::DidOom {
124755979ddSNick Fitzgerald allow_alloc: allow_alloc_after,
125755979ddSNick Fitzgerald };
126cf48211cSNick Fitzgerald ptr = ptr::null_mut();
127cf48211cSNick Fitzgerald }
128071704b4SNick Fitzgerald
129755979ddSNick Fitzgerald OomState::OomOnAlloc {
130755979ddSNick Fitzgerald counter: c,
131755979ddSNick Fitzgerald allow_alloc_after,
132755979ddSNick Fitzgerald } => {
133755979ddSNick Fitzgerald new_state = OomState::OomOnAlloc {
134755979ddSNick Fitzgerald counter: c - 1,
135755979ddSNick Fitzgerald allow_alloc_after,
136755979ddSNick Fitzgerald };
137cf48211cSNick Fitzgerald ptr = unsafe { std::alloc::System.alloc(layout) };
138cf48211cSNick Fitzgerald }
139071704b4SNick Fitzgerald
140755979ddSNick Fitzgerald OomState::DidOom { allow_alloc } => {
141b236888cSNick Fitzgerald log::trace!("Attempt to allocate {layout:?} after OOM:\n{bt}");
142755979ddSNick Fitzgerald if allow_alloc {
143755979ddSNick Fitzgerald new_state = OomState::DidOom { allow_alloc: true };
144755979ddSNick Fitzgerald ptr = ptr::null_mut();
145755979ddSNick Fitzgerald } else {
146755979ddSNick Fitzgerald panic!(
147755979ddSNick Fitzgerald "OOM test attempted to allocate after OOM: {layout:?}\n\
148755979ddSNick Fitzgerald \n\
149755979ddSNick Fitzgerald Hint: if this is acceptable, configure the OOM test to allow allocation \
150755979ddSNick Fitzgerald after OOM with `OomTest::allow_alloc_after_oom`"
151755979ddSNick Fitzgerald )
152755979ddSNick Fitzgerald }
153cf48211cSNick Fitzgerald }
154cf48211cSNick Fitzgerald }
155cf48211cSNick Fitzgerald }
156cf48211cSNick Fitzgerald
157cf48211cSNick Fitzgerald set_oom_state(new_state);
158cf48211cSNick Fitzgerald ptr
159cf48211cSNick Fitzgerald }
160cf48211cSNick Fitzgerald
dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout)161cf48211cSNick Fitzgerald unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
162cf48211cSNick Fitzgerald unsafe {
163cf48211cSNick Fitzgerald std::alloc::System.dealloc(ptr, layout);
164cf48211cSNick Fitzgerald }
165cf48211cSNick Fitzgerald }
166cf48211cSNick Fitzgerald }
167cf48211cSNick Fitzgerald
168cf48211cSNick Fitzgerald /// A test helper that checks that some code handles OOM correctly.
169cf48211cSNick Fitzgerald ///
170cf48211cSNick Fitzgerald /// `OomTest` will only work correctly when `OomTestAllocator` is configured as
171cf48211cSNick Fitzgerald /// the global allocator.
172cf48211cSNick Fitzgerald ///
173cf48211cSNick Fitzgerald /// `OomTest` does not support reentrancy, so you cannot run an `OomTest` within
174cf48211cSNick Fitzgerald /// an `OomTest`.
175cf48211cSNick Fitzgerald ///
176cf48211cSNick Fitzgerald /// # Example
177cf48211cSNick Fitzgerald ///
178cf48211cSNick Fitzgerald /// ```no_run
179cf48211cSNick Fitzgerald /// use std::time::Duration;
180cf48211cSNick Fitzgerald /// use wasmtime::Result;
181cf48211cSNick Fitzgerald /// use wasmtime_fuzzing::oom::{OomTest, OomTestAllocator};
182cf48211cSNick Fitzgerald ///
183cf48211cSNick Fitzgerald /// #[global_allocator]
184ab78bd82SHo Kim /// static GLOBAL_ALLOCATOR: OomTestAllocator = OomTestAllocator::new();
185cf48211cSNick Fitzgerald ///
186cf48211cSNick Fitzgerald /// #[test]
187cf48211cSNick Fitzgerald /// fn my_oom_test() -> Result<()> {
188cf48211cSNick Fitzgerald /// OomTest::new()
189cf48211cSNick Fitzgerald /// .max_iters(1_000_000)
190cf48211cSNick Fitzgerald /// .max_duration(Duration::from_secs(5))
191755979ddSNick Fitzgerald /// .allow_alloc_after_oom(true)
192cf48211cSNick Fitzgerald /// .test(|| {
193cf48211cSNick Fitzgerald /// todo!("insert code here that should handle OOM here...")
194cf48211cSNick Fitzgerald /// })
195cf48211cSNick Fitzgerald /// }
196cf48211cSNick Fitzgerald /// ```
197cf48211cSNick Fitzgerald pub struct OomTest {
198cf48211cSNick Fitzgerald max_iters: Option<u32>,
199cf48211cSNick Fitzgerald max_duration: Option<time::Duration>,
200755979ddSNick Fitzgerald allow_alloc_after_oom: bool,
201cf48211cSNick Fitzgerald }
202cf48211cSNick Fitzgerald
203cf48211cSNick Fitzgerald impl OomTest {
204cf48211cSNick Fitzgerald /// Create a new OOM test.
205cf48211cSNick Fitzgerald ///
206cf48211cSNick Fitzgerald /// By default there is no iteration or time limit, tests will be executed
207cf48211cSNick Fitzgerald /// until the pass (or fail).
new() -> Self208cf48211cSNick Fitzgerald pub fn new() -> Self {
209cf48211cSNick Fitzgerald let _ = env_logger::try_init();
210dfdd6539SNick Fitzgerald
211dfdd6539SNick Fitzgerald // NB: `std::backtrace::Backtrace` doesn't have ways to handle
212dfdd6539SNick Fitzgerald // OOM. Ideally we would just disable the `"backtrace"` cargo feature,
213dfdd6539SNick Fitzgerald // but workspace feature resolution doesn't play nice with that.
2148325e1ecSAlex Crichton wasmtime_core::error::disable_backtrace();
215dfdd6539SNick Fitzgerald
216cf48211cSNick Fitzgerald OomTest {
217cf48211cSNick Fitzgerald max_iters: None,
218cf48211cSNick Fitzgerald max_duration: None,
219755979ddSNick Fitzgerald allow_alloc_after_oom: false,
220cf48211cSNick Fitzgerald }
221cf48211cSNick Fitzgerald }
222cf48211cSNick Fitzgerald
223cf48211cSNick Fitzgerald /// Configure the maximum number of times to run an OOM test.
max_iters(&mut self, max_iters: u32) -> &mut Self224cf48211cSNick Fitzgerald pub fn max_iters(&mut self, max_iters: u32) -> &mut Self {
225cf48211cSNick Fitzgerald self.max_iters = Some(max_iters);
226cf48211cSNick Fitzgerald self
227cf48211cSNick Fitzgerald }
228cf48211cSNick Fitzgerald
229cf48211cSNick Fitzgerald /// Configure the maximum duration of time to run an OOM text.
max_duration(&mut self, max_duration: time::Duration) -> &mut Self230cf48211cSNick Fitzgerald pub fn max_duration(&mut self, max_duration: time::Duration) -> &mut Self {
231cf48211cSNick Fitzgerald self.max_duration = Some(max_duration);
232cf48211cSNick Fitzgerald self
233cf48211cSNick Fitzgerald }
234cf48211cSNick Fitzgerald
235755979ddSNick Fitzgerald /// Configure whether to allow allocation attempts after an OOM has already
236755979ddSNick Fitzgerald /// been injected.
237755979ddSNick Fitzgerald ///
238755979ddSNick Fitzgerald /// The default is `false`.
allow_alloc_after_oom(&mut self, allow: bool) -> &mut Self239755979ddSNick Fitzgerald pub fn allow_alloc_after_oom(&mut self, allow: bool) -> &mut Self {
240755979ddSNick Fitzgerald self.allow_alloc_after_oom = allow;
241755979ddSNick Fitzgerald self
242755979ddSNick Fitzgerald }
243755979ddSNick Fitzgerald
244cf48211cSNick Fitzgerald /// Repeatedly run the given test function, injecting OOMs at different
245cf48211cSNick Fitzgerald /// times and checking that it correctly handles them.
246cf48211cSNick Fitzgerald ///
247cf48211cSNick Fitzgerald /// The test function should not use threads, or else allocations may not be
248cf48211cSNick Fitzgerald /// tracked correctly and OOM injection may be incorrect.
249cf48211cSNick Fitzgerald ///
250cf48211cSNick Fitzgerald /// The test function should return an `Err(_)` if and only if it encounters
251cf48211cSNick Fitzgerald /// an OOM.
252cf48211cSNick Fitzgerald ///
253cf48211cSNick Fitzgerald /// Returns early once the test function returns `Ok(())` before an OOM has
254cf48211cSNick Fitzgerald /// been injected.
test(&self, test_func: impl Fn() -> Result<()>) -> Result<()>255cf48211cSNick Fitzgerald pub fn test(&self, test_func: impl Fn() -> Result<()>) -> Result<()> {
256*5c68fe60SNick Fitzgerald let future = self.test_async(|| async { test_func() });
257*5c68fe60SNick Fitzgerald crate::block_on(future)
258*5c68fe60SNick Fitzgerald }
259*5c68fe60SNick Fitzgerald
260*5c68fe60SNick Fitzgerald /// The same as `test` but `async`.
test_async(&self, test_func: impl AsyncFn() -> Result<()>) -> Result<()>261*5c68fe60SNick Fitzgerald pub async fn test_async(&self, test_func: impl AsyncFn() -> Result<()>) -> Result<()> {
262cf48211cSNick Fitzgerald let start = time::Instant::now();
263cf48211cSNick Fitzgerald
264cf48211cSNick Fitzgerald for i in 0.. {
265cf48211cSNick Fitzgerald if self.max_iters.is_some_and(|n| i >= n)
266cf48211cSNick Fitzgerald || self.max_duration.is_some_and(|d| start.elapsed() >= d)
267cf48211cSNick Fitzgerald {
268cf48211cSNick Fitzgerald break;
269cf48211cSNick Fitzgerald }
270cf48211cSNick Fitzgerald
271cf48211cSNick Fitzgerald log::trace!("=== Injecting OOM after {i} allocations ===");
272*5c68fe60SNick Fitzgerald
273*5c68fe60SNick Fitzgerald let future = std::pin::pin!(test_func());
274*5c68fe60SNick Fitzgerald let (result, oom_state) = OomTestFuture::new(
275*5c68fe60SNick Fitzgerald future,
276*5c68fe60SNick Fitzgerald OomState::OomOnAlloc {
277755979ddSNick Fitzgerald counter: i,
278755979ddSNick Fitzgerald allow_alloc_after: self.allow_alloc_after_oom,
279*5c68fe60SNick Fitzgerald },
280*5c68fe60SNick Fitzgerald )
281*5c68fe60SNick Fitzgerald .await;
282cf48211cSNick Fitzgerald
283*5c68fe60SNick Fitzgerald match (result, oom_state) {
284cf48211cSNick Fitzgerald (_, OomState::OutsideOomTest) => unreachable!(),
285cf48211cSNick Fitzgerald
286cf48211cSNick Fitzgerald // The test function completed successfully before we ran out of
287cf48211cSNick Fitzgerald // allocation fuel, so we're done.
288755979ddSNick Fitzgerald (Ok(()), OomState::OomOnAlloc { .. }) => break,
289cf48211cSNick Fitzgerald
290cf48211cSNick Fitzgerald // We injected an OOM and the test function handled it
291cf48211cSNick Fitzgerald // correctly; continue to the next iteration.
292*5c68fe60SNick Fitzgerald (Err(e), OomState::DidOom { .. }) if e.is::<OutOfMemory>() => {}
293cf48211cSNick Fitzgerald
294cf48211cSNick Fitzgerald // Missed OOMs.
295755979ddSNick Fitzgerald (Ok(()), OomState::DidOom { .. }) => {
296cf48211cSNick Fitzgerald bail!("OOM test function missed an OOM: returned Ok(())");
297cf48211cSNick Fitzgerald }
298755979ddSNick Fitzgerald (Err(e), OomState::DidOom { .. }) => {
299cf48211cSNick Fitzgerald return Err(
300cf48211cSNick Fitzgerald e.context("OOM test function missed an OOM: returned non-OOM error")
301cf48211cSNick Fitzgerald );
302cf48211cSNick Fitzgerald }
303cf48211cSNick Fitzgerald
304cf48211cSNick Fitzgerald // Unexpected error.
305755979ddSNick Fitzgerald (Err(e), OomState::OomOnAlloc { .. }) => {
306cf48211cSNick Fitzgerald return Err(
307cf48211cSNick Fitzgerald e.context("OOM test function returned an error when there was no OOM")
308cf48211cSNick Fitzgerald );
309cf48211cSNick Fitzgerald }
310cf48211cSNick Fitzgerald }
311cf48211cSNick Fitzgerald }
312cf48211cSNick Fitzgerald
313cf48211cSNick Fitzgerald Ok(())
314cf48211cSNick Fitzgerald }
315*5c68fe60SNick Fitzgerald }
316cf48211cSNick Fitzgerald
317*5c68fe60SNick Fitzgerald struct OomTestFuture<'a, F> {
318*5c68fe60SNick Fitzgerald inner: Pin<&'a mut F>,
319*5c68fe60SNick Fitzgerald state: OomState,
320*5c68fe60SNick Fitzgerald }
321*5c68fe60SNick Fitzgerald
322*5c68fe60SNick Fitzgerald impl<'a, F> OomTestFuture<'a, F> {
new(inner: Pin<&'a mut F>, state: OomState) -> Self323*5c68fe60SNick Fitzgerald fn new(inner: Pin<&'a mut F>, state: OomState) -> Self {
324*5c68fe60SNick Fitzgerald OomTestFuture { inner, state }
325*5c68fe60SNick Fitzgerald }
326*5c68fe60SNick Fitzgerald }
327*5c68fe60SNick Fitzgerald
328*5c68fe60SNick Fitzgerald impl<F> Future for OomTestFuture<'_, F>
329*5c68fe60SNick Fitzgerald where
330*5c68fe60SNick Fitzgerald F: Future<Output = Result<()>>,
331*5c68fe60SNick Fitzgerald {
332*5c68fe60SNick Fitzgerald type Output = (Result<()>, OomState);
333*5c68fe60SNick Fitzgerald
poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>334*5c68fe60SNick Fitzgerald fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
335*5c68fe60SNick Fitzgerald log::trace!("OomTestFuture::poll: Entered ---");
336*5c68fe60SNick Fitzgerald
337*5c68fe60SNick Fitzgerald let result = {
338*5c68fe60SNick Fitzgerald let OomTestFuture { inner, state } = &mut *self;
339*5c68fe60SNick Fitzgerald
340*5c68fe60SNick Fitzgerald let guard = ScopedOomState::new(state);
341*5c68fe60SNick Fitzgerald assert_eq!(*guard.state, OomState::OutsideOomTest);
342*5c68fe60SNick Fitzgerald
343*5c68fe60SNick Fitzgerald inner.as_mut().poll(cx)
344*5c68fe60SNick Fitzgerald };
345*5c68fe60SNick Fitzgerald
346*5c68fe60SNick Fitzgerald log::trace!("OomTestFuture::poll: inner result: {result:?}");
347*5c68fe60SNick Fitzgerald log::trace!("OomTestFuture::poll: old state: {:?}", self.state);
348*5c68fe60SNick Fitzgerald
349*5c68fe60SNick Fitzgerald match result {
350*5c68fe60SNick Fitzgerald Poll::Pending => {
351*5c68fe60SNick Fitzgerald set_oom_state(OomState::OutsideOomTest);
352*5c68fe60SNick Fitzgerald Poll::Pending
353*5c68fe60SNick Fitzgerald }
354*5c68fe60SNick Fitzgerald Poll::Ready(result) => Poll::Ready((result, mem::take(&mut self.state))),
355*5c68fe60SNick Fitzgerald }
356cf48211cSNick Fitzgerald }
357cf48211cSNick Fitzgerald }
358