Skip to main content

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