system_update_committer/
metadata.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Handles interfacing with the boot metadata (e.g. verifying a slot, committing a slot, etc).
6
7use commit::do_commit;
8use errors::MetadataError;
9use fidl_fuchsia_update_verify::HealthVerificationProxy;
10use fuchsia_async::TimeoutExt as _;
11use futures::channel::oneshot;
12use policy::PolicyEngine;
13use std::time::Instant;
14use zx::{EventPair, Peered};
15use {fidl_fuchsia_paver as fpaver, fuchsia_inspect as finspect};
16
17mod commit;
18mod configuration_without_recovery;
19mod errors;
20mod inspect;
21mod policy;
22
23/// Puts BootManager metadata into a happy state, provided we believe the system can OTA.
24///
25/// The "happy state" is:
26/// * The current configuration is active and marked Healthy.
27/// * The alternate configuration is marked Unbootable.
28///
29/// To put the metadata in this state, we may need to verify and commit. To make it easier to
30/// determine if we should verify and commit, we consult the `PolicyEngine`.
31///
32/// If this function returns an error, it likely means that the system is somehow busted, and that
33/// it should be rebooted. Rebooting will hopefully either fix the issue or decrement the boot
34/// counter, eventually leading to a rollback.
35pub async fn put_metadata_in_happy_state(
36    boot_manager: &fpaver::BootManagerProxy,
37    p_internal: &EventPair,
38    unblocker: oneshot::Sender<()>,
39    health_verification: &HealthVerificationProxy,
40    commit_timeout: zx::MonotonicDuration,
41    node: &finspect::Node,
42    commit_inspect: &CommitInspect,
43) -> Result<CommitResult, MetadataError> {
44    let start_time = Instant::now();
45    let res = put_metadata_in_happy_state_impl(
46        boot_manager,
47        p_internal,
48        unblocker,
49        health_verification,
50        commit_inspect,
51    )
52    .on_timeout(commit_timeout, || Err(MetadataError::Timeout))
53    .await;
54
55    match &res {
56        Ok(CommitResult::CommitNotNecessary) => (),
57        Ok(CommitResult::CommittedSystem) | Err(_) => {
58            inspect::write_to_inspect(node, res.as_ref().map(|_| ()), start_time.elapsed())
59        }
60    }
61
62    res
63}
64
65async fn put_metadata_in_happy_state_impl(
66    boot_manager: &fpaver::BootManagerProxy,
67    p_internal: &EventPair,
68    unblocker: oneshot::Sender<()>,
69    health_verification: &HealthVerificationProxy,
70    commit_inspect: &CommitInspect,
71) -> Result<CommitResult, MetadataError> {
72    let mut unblocker = Some(unblocker);
73    let commit_result = {
74        let engine = PolicyEngine::build(boot_manager).await.map_err(MetadataError::Policy)?;
75        if let Some((current_config, boot_attempts)) = engine.should_verify_and_commit() {
76            // At this point, the FIDL server should start responding to requests so that clients
77            // can find out that the health verification is underway.
78            unblocker = unblock_fidl_server(unblocker)?;
79            let () =
80                zx::Status::ok(health_verification.query_health_checks().await.map_err(|e| {
81                    MetadataError::HealthVerification(errors::HealthVerificationError::Fidl(e))
82                })?)
83                .map_err(|e| {
84                    MetadataError::HealthVerification(errors::HealthVerificationError::Unhealthy(e))
85                })?;
86            let () =
87                do_commit(boot_manager, current_config).await.map_err(MetadataError::Commit)?;
88            let () = commit_inspect.record_boot_attempts(boot_attempts);
89            CommitResult::CommittedSystem
90        } else {
91            CommitResult::CommitNotNecessary
92        }
93    };
94
95    // Tell the rest of the system we are now committed.
96    let () = p_internal
97        .signal_peer(zx::Signals::NONE, zx::Signals::USER_0)
98        .map_err(MetadataError::SignalPeer)?;
99
100    // Ensure the FIDL server will be unblocked, even if we didn't verify health.
101    unblock_fidl_server(unblocker)?;
102
103    Ok(commit_result)
104}
105
106#[derive(Debug)]
107pub enum CommitResult {
108    CommittedSystem,
109    CommitNotNecessary,
110}
111
112impl CommitResult {
113    pub fn log_msg(&self) -> &'static str {
114        match self {
115            Self::CommittedSystem => "Committed system.",
116            Self::CommitNotNecessary => "Commit not necessary.",
117        }
118    }
119}
120
121/// Records inspect data specific to committing the update if the health checks pass.
122pub struct CommitInspect(finspect::Node);
123
124impl CommitInspect {
125    pub fn new(node: finspect::Node) -> Self {
126        Self(node)
127    }
128
129    fn record_boot_attempts(&self, count: Option<u8>) {
130        match count {
131            Some(count) => self.0.record_uint("boot_attempts", count.into()),
132            None => self.0.record_uint("boot_attempts_missing", 0),
133        }
134    }
135}
136
137fn unblock_fidl_server(
138    unblocker: Option<oneshot::Sender<()>>,
139) -> Result<Option<oneshot::Sender<()>>, MetadataError> {
140    if let Some(sender) = unblocker {
141        let () = sender.send(()).map_err(|_| MetadataError::Unblock)?;
142    }
143    Ok(None)
144}
145
146// There is intentionally some overlap between the tests here and in `policy`. We do this so we can
147// test the functionality at different layers.
148#[cfg(test)]
149mod tests {
150    use super::errors::HealthVerificationError;
151    use super::*;
152    use assert_matches::assert_matches;
153    use configuration_without_recovery::ConfigurationWithoutRecovery;
154    use fasync::OnSignals;
155    use fuchsia_async as fasync;
156    use mock_health_verification::MockHealthVerificationService;
157    use mock_paver::{hooks as mphooks, MockPaverServiceBuilder, PaverEvent};
158    use std::sync::atomic::{AtomicU32, Ordering};
159    use std::sync::Arc;
160    use zx::{AsHandleRef, Status};
161
162    fn health_verification_and_call_count(
163        status: zx::Status,
164    ) -> (HealthVerificationProxy, Arc<AtomicU32>) {
165        let call_count = Arc::new(AtomicU32::new(0));
166        let call_count_clone = Arc::clone(&call_count);
167        let verification = Arc::new(MockHealthVerificationService::new(move || {
168            call_count_clone.fetch_add(1, Ordering::SeqCst);
169            status
170        }));
171
172        let (health_verification, server) = verification.spawn_health_verification_service();
173        let () = server.detach();
174
175        (health_verification, call_count)
176    }
177
178    /// When we don't support ABR, we should not update metadata.
179    /// However, the FIDL server should still be unblocked.
180    #[fasync::run_singlethreaded(test)]
181    async fn test_does_not_change_metadata_when_device_does_not_support_abr() {
182        let paver = Arc::new(
183            MockPaverServiceBuilder::new()
184                .boot_manager_close_with_epitaph(Status::NOT_SUPPORTED)
185                .build(),
186        );
187        let (p_internal, p_external) = EventPair::create();
188        let (unblocker, unblocker_recv) = oneshot::channel();
189        let (health_verification, health_verification_call_count) =
190            health_verification_and_call_count(zx::Status::OK);
191
192        put_metadata_in_happy_state_impl(
193            &paver.spawn_boot_manager_service(),
194            &p_internal,
195            unblocker,
196            &health_verification,
197            &CommitInspect::new(finspect::Node::default()),
198        )
199        .await
200        .unwrap();
201
202        assert_eq!(paver.take_events(), vec![]);
203        assert_eq!(
204            p_external
205                .wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
206                .to_result(),
207            Ok(zx::Signals::USER_0)
208        );
209        assert_eq!(unblocker_recv.await, Ok(()));
210        assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
211    }
212
213    /// When we're in recovery, we should not update metadata.
214    /// However, the FIDL server should still be unblocked.
215    #[fasync::run_singlethreaded(test)]
216    async fn test_does_not_change_metadata_when_device_in_recovery() {
217        let paver = Arc::new(
218            MockPaverServiceBuilder::new()
219                .current_config(fpaver::Configuration::Recovery)
220                .insert_hook(mphooks::config_status(|_| Ok(fpaver::ConfigurationStatus::Healthy)))
221                .build(),
222        );
223        let (p_internal, p_external) = EventPair::create();
224        let (unblocker, unblocker_recv) = oneshot::channel();
225        let (health_verification, health_verification_call_count) =
226            health_verification_and_call_count(zx::Status::OK);
227
228        put_metadata_in_happy_state_impl(
229            &paver.spawn_boot_manager_service(),
230            &p_internal,
231            unblocker,
232            &health_verification,
233            &CommitInspect::new(finspect::Node::default()),
234        )
235        .await
236        .unwrap();
237
238        assert_eq!(paver.take_events(), vec![PaverEvent::QueryCurrentConfiguration]);
239        assert_eq!(
240            p_external
241                .wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
242                .to_result(),
243            Ok(zx::Signals::USER_0)
244        );
245        assert_eq!(unblocker_recv.await, Ok(()));
246        assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
247    }
248
249    /// When the current slot is healthy, we should not update metadata.
250    /// However, the FIDL server should still be unblocked.
251    async fn test_does_not_change_metadata_when_current_is_healthy(
252        current_config: &ConfigurationWithoutRecovery,
253    ) {
254        let paver = Arc::new(
255            MockPaverServiceBuilder::new()
256                .current_config(current_config.into())
257                .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
258                    Ok((fpaver::ConfigurationStatus::Healthy, None))
259                }))
260                .build(),
261        );
262        let (p_internal, p_external) = EventPair::create();
263        let (unblocker, unblocker_recv) = oneshot::channel();
264        let (health_verification, health_verification_call_count) =
265            health_verification_and_call_count(zx::Status::OK);
266
267        put_metadata_in_happy_state_impl(
268            &paver.spawn_boot_manager_service(),
269            &p_internal,
270            unblocker,
271            &health_verification,
272            &CommitInspect::new(finspect::Node::default()),
273        )
274        .await
275        .unwrap();
276
277        assert_eq!(
278            paver.take_events(),
279            vec![
280                PaverEvent::QueryCurrentConfiguration,
281                PaverEvent::QueryConfigurationStatusAndBootAttempts {
282                    configuration: current_config.into()
283                }
284            ]
285        );
286        assert_eq!(
287            p_external
288                .wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
289                .to_result(),
290            Ok(zx::Signals::USER_0)
291        );
292        assert_eq!(unblocker_recv.await, Ok(()));
293        assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
294    }
295
296    #[fasync::run_singlethreaded(test)]
297    async fn test_does_not_change_metadata_when_current_is_healthy_a() {
298        test_does_not_change_metadata_when_current_is_healthy(&ConfigurationWithoutRecovery::A)
299            .await
300    }
301
302    #[fasync::run_singlethreaded(test)]
303    async fn test_does_not_change_metadata_when_current_is_healthy_b() {
304        test_does_not_change_metadata_when_current_is_healthy(&ConfigurationWithoutRecovery::B)
305            .await
306    }
307
308    /// When the current slot is pending, we should verify, commit, & unblock the fidl server.
309    async fn test_verifies_and_commits_when_current_is_pending(
310        current_config: &ConfigurationWithoutRecovery,
311    ) {
312        let paver = Arc::new(
313            MockPaverServiceBuilder::new()
314                .current_config(current_config.into())
315                .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
316                    Ok((fpaver::ConfigurationStatus::Pending, Some(1)))
317                }))
318                .build(),
319        );
320        let (p_internal, p_external) = EventPair::create();
321        let (unblocker, unblocker_recv) = oneshot::channel();
322        let (health_verification, health_verification_call_count) =
323            health_verification_and_call_count(zx::Status::OK);
324
325        put_metadata_in_happy_state_impl(
326            &paver.spawn_boot_manager_service(),
327            &p_internal,
328            unblocker,
329            &health_verification,
330            &CommitInspect::new(finspect::Node::default()),
331        )
332        .await
333        .unwrap();
334
335        assert_eq!(
336            paver.take_events(),
337            vec![
338                PaverEvent::QueryCurrentConfiguration,
339                PaverEvent::QueryConfigurationStatusAndBootAttempts {
340                    configuration: current_config.into()
341                },
342                PaverEvent::SetConfigurationHealthy { configuration: current_config.into() },
343                PaverEvent::SetConfigurationUnbootable {
344                    configuration: current_config.to_alternate().into()
345                },
346                PaverEvent::BootManagerFlush,
347            ]
348        );
349        assert_eq!(OnSignals::new(&p_external, zx::Signals::USER_0).await, Ok(zx::Signals::USER_0));
350        assert_eq!(unblocker_recv.await, Ok(()));
351        assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 1);
352    }
353
354    #[fasync::run_singlethreaded(test)]
355    async fn test_verifies_and_commits_when_current_is_pending_a() {
356        test_verifies_and_commits_when_current_is_pending(&ConfigurationWithoutRecovery::A).await
357    }
358
359    #[fasync::run_singlethreaded(test)]
360    async fn test_verifies_and_commits_when_current_is_pending_b() {
361        test_verifies_and_commits_when_current_is_pending(&ConfigurationWithoutRecovery::B).await
362    }
363
364    /// When we fail to verify and the config says to ignore, we should still do the commit.
365    #[fasync::run_singlethreaded(test)]
366    async fn test_commits_when_failed_verification_ignored() {
367        let paver = Arc::new(
368            MockPaverServiceBuilder::new()
369                .current_config(fpaver::Configuration::Recovery)
370                .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
371                    Ok((fpaver::ConfigurationStatus::Pending, Some(1)))
372                }))
373                .build(),
374        );
375        let (p_internal, p_external) = EventPair::create();
376        let (unblocker, unblocker_recv) = oneshot::channel();
377        let (health_verification, health_verification_call_count) =
378            health_verification_and_call_count(zx::Status::INTERNAL);
379
380        put_metadata_in_happy_state_impl(
381            &paver.spawn_boot_manager_service(),
382            &p_internal,
383            unblocker,
384            &health_verification,
385            &CommitInspect::new(finspect::Node::default()),
386        )
387        .await
388        .unwrap();
389
390        assert_eq!(paver.take_events(), vec![PaverEvent::QueryCurrentConfiguration,]);
391        assert_eq!(OnSignals::new(&p_external, zx::Signals::USER_0).await, Ok(zx::Signals::USER_0));
392        assert_eq!(unblocker_recv.await, Ok(()));
393        assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 0);
394    }
395
396    /// When we fail to verify and the config says to not ignore, we should report an error.
397    async fn test_errors_when_failed_verification_not_ignored(
398        current_config: &ConfigurationWithoutRecovery,
399    ) {
400        let paver = Arc::new(
401            MockPaverServiceBuilder::new()
402                .current_config(current_config.into())
403                .insert_hook(mphooks::config_status_and_boot_attempts(|_| {
404                    Ok((fpaver::ConfigurationStatus::Pending, Some(1)))
405                }))
406                .build(),
407        );
408        let (p_internal, p_external) = EventPair::create();
409        let (unblocker, unblocker_recv) = oneshot::channel();
410        let (health_verification, health_verification_call_count) =
411            health_verification_and_call_count(zx::Status::INTERNAL);
412
413        let result = put_metadata_in_happy_state_impl(
414            &paver.spawn_boot_manager_service(),
415            &p_internal,
416            unblocker,
417            &health_verification,
418            &CommitInspect::new(finspect::Node::default()),
419        )
420        .await;
421
422        assert_matches!(
423            result,
424            Err(MetadataError::HealthVerification(HealthVerificationError::Unhealthy(
425                Status::INTERNAL
426            )))
427        );
428
429        assert_eq!(
430            paver.take_events(),
431            vec![
432                PaverEvent::QueryCurrentConfiguration,
433                PaverEvent::QueryConfigurationStatusAndBootAttempts {
434                    configuration: current_config.into()
435                },
436            ]
437        );
438        assert_eq!(
439            p_external
440                .wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE_PAST)
441                .to_result(),
442            Err(zx::Status::TIMED_OUT)
443        );
444        assert_eq!(unblocker_recv.await, Ok(()));
445        assert_eq!(health_verification_call_count.load(Ordering::SeqCst), 1);
446    }
447
448    #[fasync::run_singlethreaded(test)]
449    async fn test_errors_when_failed_verification_not_ignored_a() {
450        test_errors_when_failed_verification_not_ignored(&ConfigurationWithoutRecovery::A).await
451    }
452
453    #[fasync::run_singlethreaded(test)]
454    async fn test_errors_when_failed_verification_not_ignored_b() {
455        test_errors_when_failed_verification_not_ignored(&ConfigurationWithoutRecovery::B).await
456    }
457
458    #[fasync::run_singlethreaded(test)]
459    async fn commit_inspect_handles_missing_count() {
460        let inspector = finspect::Inspector::default();
461        let commit_inspect = CommitInspect::new(inspector.root().create_child("commit"));
462
463        commit_inspect.record_boot_attempts(None);
464
465        diagnostics_assertions::assert_data_tree!(inspector, root: {
466            "commit": {
467                "boot_attempts_missing": 0u64
468            }
469        });
470    }
471}