Skip to content

Commit f77b092

Browse files
committed
Fix some race conditions between the add-app and update procedures.
1 parent 252d7a0 commit f77b092

5 files changed

Lines changed: 217 additions & 10 deletions

File tree

app_native/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,8 @@ pub fn process_heartbeat_config_response(
718718

719719
for i in 0..add_app_resps_com.len() {
720720
if i < NUM_COMMON_MLS_CLIENTS {
721-
// Merge the psk_proposal and commit for the add operation
721+
// Store update proposals, merge the psk_proposal, and commit for the add operation
722+
clients.as_mut().unwrap().mls_clients[i].store_update_proposals(add_app_resps_com[i].update_proposals_vec.clone()).unwrap();
722723
clients.as_mut().unwrap().mls_clients[i].decrypt(add_app_resps_com[i].psk_proposal_vec.clone(), false).unwrap();
723724
clients.as_mut().unwrap().mls_clients[i].decrypt_with_secret(add_app_resps_com[i].commit_msg_vec.clone(), false, secret.clone()).unwrap();
724725
clients.as_mut().unwrap().mls_clients[i].save_group_state().unwrap();
@@ -827,7 +828,8 @@ pub fn process_add_app_config_response(
827828

828829
let new_app_data: [NewAppData; NUM_MLS_CLIENTS] = std::array::from_fn(|i| {
829830
if i < NUM_COMMON_MLS_CLIENTS {
830-
// Merge the psk_proposal and commit for the add operation
831+
// Store update proposals, merge the psk_proposal, and commit for the add operation
832+
clients.as_mut().unwrap().mls_clients[i].store_update_proposals(add_app_resps_com[i].update_proposals_vec.clone()).unwrap();
831833
clients.as_mut().unwrap().mls_clients[i].decrypt(add_app_resps_com[i].psk_proposal_vec.clone(), false).unwrap();
832834
clients.as_mut().unwrap().mls_clients[i].decrypt_with_secret(add_app_resps_com[i].commit_msg_vec.clone(), false, secret.clone()).unwrap();
833835
clients.as_mut().unwrap().mls_clients[i].save_group_state().unwrap();

camera_hub/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ fn handle_add_app_request(
161161
)
162162
.unwrap();
163163

164+
let update_proposals_vec = clients_com[i].get_update_proposals().unwrap();
165+
164166
let (welcome_msg_vec, psk_proposal_vec, commit_msg_vec) = clients_com[i]
165167
.invite_with_secret(&camera_contact, secret.clone())
166168
.unwrap();
@@ -172,6 +174,7 @@ fn handle_add_app_request(
172174
welcome_msg_vec,
173175
psk_proposal_vec,
174176
commit_msg_vec,
177+
update_proposals_vec,
175178
}
176179
});
177180

client_lib/src/config.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use crate::mls_clients::{MlsClients, MLS_CLIENT_TAGS, MOTION, NUM_MLS_CLIENTS, THUMBNAIL,
66
MlsClientsCommon, MlsClientsDedicated, NUM_COMMON_MLS_CLIENTS, LIVESTREAM_DED};
7-
use openmls::prelude::KeyPackage;
7+
use openmls::prelude::{KeyPackage, QueuedProposal};
88
use log::{error, info};
99
use serde::{Deserialize, Serialize};
1010
use std::io;
@@ -67,13 +67,17 @@ impl HeartbeatRequest {
6767
if MLS_CLIENT_TAGS[i] == "motion"
6868
|| MLS_CLIENT_TAGS[i] == "thumbnail"
6969
{
70+
// It is possible that the update proposal races with an add app commit and end up
71+
// being in an older epoch. We just ignore such update proposals. The app will generate
72+
// another update in the next heartbeat.
7073
let _ =
71-
clients_com[i].decrypt(self.update_proposals[proposals_i].clone(), false)?;
74+
clients_com[i].decrypt(self.update_proposals[proposals_i].clone(), false);
7275
clients_com[i].save_group_state().unwrap();
7376
proposals_i += 1;
7477
} else if MLS_CLIENT_TAGS[i] == "livestream" {
78+
// Same comment here as above.
7579
let _ =
76-
clients_ded[i - NUM_COMMON_MLS_CLIENTS].decrypt(self.update_proposals[proposals_i].clone(), false)?;
80+
clients_ded[i - NUM_COMMON_MLS_CLIENTS].decrypt(self.update_proposals[proposals_i].clone(), false);
7781
clients_ded[i - NUM_COMMON_MLS_CLIENTS].save_group_state().unwrap();
7882
proposals_i += 1;
7983
}
@@ -229,6 +233,7 @@ pub struct AddAppResponseCommon {
229233
pub welcome_msg_vec: Vec<u8>,
230234
pub psk_proposal_vec: Vec<u8>,
231235
pub commit_msg_vec: Vec<u8>,
236+
pub update_proposals_vec: Vec<QueuedProposal>,
232237
}
233238

234239
#[derive(Serialize, Deserialize)]

client_lib/src/mls_client.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@ impl MlsClient {
283283
group.mls_group.set_aad(group_aad.as_bytes().to_vec());
284284
}
285285

286+
for contact in &mut group.contacts {
287+
if let Some(proposal) = contact.update_proposal.take() {
288+
group
289+
.mls_group
290+
.store_pending_proposal(self.provider.storage(), proposal)
291+
.map_err(|e| io::Error::other(format!("Error: could not store proposal - {e}")))?;
292+
}
293+
}
294+
286295
// Build a proposal with this key package and do the MLS bits.
287296
let joiner_key_package = contact.key_package.clone();
288297

@@ -1021,7 +1030,7 @@ impl MlsClient {
10211030
|| !(staged_commit.psk_proposals().next().is_none()
10221031
|| staged_commit.psk_proposals().collect::<Vec<_>>().len() == 1)
10231032
|| !(staged_commit.queued_proposals().next().is_none()
1024-
|| staged_commit.queued_proposals().collect::<Vec<_>>().len() <= cmp::max(2, num_apps_in_group))
1033+
|| staged_commit.queued_proposals().collect::<Vec<_>>().len() <= cmp::max(2, num_apps_in_group + 2)) // 1 psk, 1 add, 1 update per member
10251034
{
10261035
return Err(io::Error::other(
10271036
"Error: staged commit message must contain at most one update/queued proposal and no other proposals.",

client_lib/src/tests.rs

Lines changed: 192 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod tests {
1111
use crate::video::{encrypt_video_file, decrypt_video_file,
1212
encrypt_thumbnail_file, decrypt_thumbnail_file};
1313
use crate::thumbnail_meta_info::ThumbnailMetaInfo;
14+
use openmls::prelude::QueuedProposal;
1415
use std::fs::{self, File};
1516
use std::io;
1617
use std::io::{Read, Write};
@@ -587,14 +588,14 @@ mod tests {
587588
assert!(msg == msg_dec);
588589
}
589590

590-
/// This function is a complete, successful pairing process with built-in secrets.
591+
/// This function is a complete, successful pairing process with the first
592+
/// secondary app (app2) with built-in secrets.
591593
/// It is used in other tests.
592-
fn pair_with_two_more_apps(
594+
fn pair_with_app2(
593595
camera: &mut MlsClient,
594596
app: &mut MlsClient,
595-
) -> (MlsClient, MlsClient) {
597+
) -> MlsClient {
596598
fs::create_dir("test_data/app2").unwrap();
597-
fs::create_dir("test_data/app3").unwrap();
598599

599600
// Add the second app
600601
let mut app2 = MlsClient::new(
@@ -616,15 +617,28 @@ mod tests {
616617
let (welcome_msg_vec, psk_proposal_vec, commit_msg_vec) = camera
617618
.invite_with_secret(&camera_contact, new_secret.clone()).unwrap();
618619
camera.save_group_state().unwrap();
620+
let update_proposals = camera.get_update_proposals().unwrap();
619621

620622
app2.process_welcome_with_secret(app2_contact, welcome_msg_vec, new_secret.clone(), GROUP_NAME).unwrap();
621623
app2.save_group_state().unwrap();
622624

623625
// App merges the psk_proposal and commit for the add operation
626+
app.store_update_proposals(update_proposals).unwrap();
624627
app.decrypt(psk_proposal_vec, false).unwrap();
625628
app.decrypt_with_secret(commit_msg_vec, false, new_secret).unwrap();
626629
app.save_group_state().unwrap();
627630

631+
app2
632+
}
633+
634+
/// This function is the first step of adding the second secondary app (app3).
635+
/// It is used in other tests.
636+
fn pair_with_app3_handshake(
637+
camera: &mut MlsClient,
638+
app: &mut MlsClient,
639+
) -> (MlsClient, Vec<u8>, Vec<u8>, Vec<QueuedProposal>) {
640+
fs::create_dir("test_data/app3").unwrap();
641+
628642
// Add the third app
629643
let mut app3 = MlsClient::new(
630644
"app3".to_string(),
@@ -641,21 +655,49 @@ mod tests {
641655

642656
let new_secret = vec![3u8; NUM_SECRET_BYTES];
643657

658+
let update_proposals = camera.get_update_proposals().unwrap();
644659
let (welcome_msg_vec, psk_proposal_vec, commit_msg_vec) = camera
645660
.invite_with_secret(&camera_contact, new_secret.clone()).unwrap();
646661
camera.save_group_state().unwrap();
647662

648663
app3.process_welcome_with_secret(app3_contact, welcome_msg_vec, new_secret.clone(), GROUP_NAME).unwrap();
649664
app3.save_group_state().unwrap();
650665

666+
app.store_update_proposals(update_proposals.clone()).unwrap();
651667
app.decrypt(psk_proposal_vec.clone(), false).unwrap();
652668
app.decrypt_with_secret(commit_msg_vec.clone(), false, new_secret.clone()).unwrap();
653669
app.save_group_state().unwrap();
670+
671+
(app3, psk_proposal_vec, commit_msg_vec, update_proposals)
672+
}
673+
674+
/// This function is the second step of adding the second secondary app (app3).
675+
/// It is used in other tests.
676+
fn pair_with_app3_inform_app2(
677+
app2: &mut MlsClient,
678+
psk_proposal_vec: Vec<u8>,
679+
commit_msg_vec: Vec<u8>,
680+
update_proposals: Vec<QueuedProposal>,
681+
) {
682+
// Must be the same as the one in pair_with_app3_handshake()
683+
let new_secret = vec![3u8; NUM_SECRET_BYTES];
654684

685+
app2.store_update_proposals(update_proposals).unwrap();
655686
app2.decrypt(psk_proposal_vec, false).unwrap();
656687
app2.decrypt_with_secret(commit_msg_vec, false, new_secret).unwrap();
657688
app2.save_group_state().unwrap();
689+
}
658690

691+
/// This function is a complete, successful pairing process with built-in secrets.
692+
/// It is used in other tests.
693+
fn pair_with_two_more_apps(
694+
camera: &mut MlsClient,
695+
app: &mut MlsClient,
696+
) -> (MlsClient, MlsClient) {
697+
let mut app2 = pair_with_app2(camera, app);
698+
let (app3, psk_proposal_vec, commit_msg_vec, update_proposals) = pair_with_app3_handshake(camera, app);
699+
pair_with_app3_inform_app2(&mut app2, psk_proposal_vec, commit_msg_vec, update_proposals);
700+
659701
(app2, app3)
660702
}
661703

@@ -1456,4 +1498,150 @@ mod tests {
14561498
check_decrypted_dummy_file(&dec_thumbnail_pathname, file_size);
14571499
}
14581500
}
1501+
1502+
/// The input app generates an update proposal and returns it.
1503+
fn app_update(
1504+
app: &mut MlsClient,
1505+
) -> Vec<u8> {
1506+
let update_proposal = app.update_proposal().unwrap();
1507+
app.save_group_state().unwrap();
1508+
1509+
update_proposal
1510+
}
1511+
1512+
/// Camera receives the update proposal.
1513+
fn camera_receive_update_proposal(
1514+
camera: &mut MlsClient,
1515+
update_proposal: Vec<u8>,
1516+
) {
1517+
camera.decrypt(update_proposal, false).unwrap();
1518+
camera.save_group_state().unwrap();
1519+
}
1520+
1521+
/// Camera receives the update proposal.
1522+
fn camera_receive_update_proposal_ignore_old(
1523+
camera: &mut MlsClient,
1524+
update_proposal: Vec<u8>,
1525+
) {
1526+
let _ = camera.decrypt(update_proposal, false);
1527+
camera.save_group_state().unwrap();
1528+
}
1529+
1530+
/// Decrypts an encrypted file and check the decrypted file.
1531+
fn decrypt_and_check_file(
1532+
app: &mut MlsClient,
1533+
name: &str,
1534+
enc_video_pathname: &str,
1535+
file_size: usize,
1536+
) {
1537+
// App decrypts video file
1538+
let dir = format!("test_data/{}/videos", name);
1539+
fs::create_dir_all(&dir).unwrap();
1540+
1541+
let dec_video_filename = decrypt_video_file(
1542+
app,
1543+
enc_video_pathname,
1544+
).unwrap();
1545+
1546+
let dec_video_pathname = format!("{}/{}", dir, dec_video_filename);
1547+
1548+
// Check decrypted file
1549+
check_decrypted_dummy_file(&dec_video_pathname, file_size);
1550+
}
1551+
1552+
#[test]
1553+
/// Functionally, this test is supposed to be similar
1554+
/// to camera_to_more_apps_update_video_test.
1555+
/// However, it is one of the tests that shuffle
1556+
/// the operations to test race conditions that we find.
1557+
/// In this specific test, app3 is added after updates by other apps,
1558+
/// but before a video is encrypted.
1559+
fn camera_to_more_apps_update_video_race_test_1() {
1560+
let (mut camera, mut app) = pair();
1561+
//let (mut app2, mut app3) = pair_with_two_more_apps(&mut camera, &mut app);
1562+
let mut app2 = pair_with_app2(&mut camera, &mut app);
1563+
1564+
// app1 update
1565+
let update_proposal = app_update(&mut app);
1566+
camera_receive_update_proposal(&mut camera, update_proposal);
1567+
1568+
// app2 update
1569+
let update_proposal = app_update(&mut app2);
1570+
camera_receive_update_proposal(&mut camera, update_proposal);
1571+
1572+
// Create input video file to be encrypted (all 0's)
1573+
let video_pathname = "test_data/video_file";
1574+
let file_size: usize = 96 * 1024 + 135;
1575+
1576+
generate_dummy_file(video_pathname, file_size);
1577+
1578+
// Add app3
1579+
let (mut app3, psk_proposal_vec, commit_msg_vec, update_proposals) = pair_with_app3_handshake(&mut camera, &mut app);
1580+
pair_with_app3_inform_app2(&mut app2, psk_proposal_vec, commit_msg_vec, update_proposals);
1581+
1582+
// Camera encrypts video file
1583+
let enc_video_pathname = "test_data/enc_video_file";
1584+
1585+
encrypt_video_file(
1586+
&mut camera,
1587+
video_pathname,
1588+
enc_video_pathname,
1589+
0,
1590+
).unwrap();
1591+
1592+
decrypt_and_check_file(&mut app, "app", enc_video_pathname, file_size);
1593+
decrypt_and_check_file(&mut app2, "app2", enc_video_pathname, file_size);
1594+
decrypt_and_check_file(&mut app3, "app3", enc_video_pathname, file_size);
1595+
}
1596+
1597+
#[test]
1598+
/// Functionally, this test is supposed to be similar
1599+
/// to camera_to_more_apps_update_video_test.
1600+
/// However, it is one of the tests that shuffle
1601+
/// the operations to test race conditions that we find.
1602+
/// In this specific test, app2 is informed of the addition of
1603+
/// app3 after app performs an update.
1604+
fn camera_to_more_apps_update_video_race_test_2() {
1605+
let (mut camera, mut app) = pair();
1606+
//let (mut app2, mut app3) = pair_with_two_more_apps(&mut camera, &mut app);
1607+
let mut app2 = pair_with_app2(&mut camera, &mut app);
1608+
let (mut app3, psk_proposal_vec, commit_msg_vec, update_proposals) = pair_with_app3_handshake(&mut camera, &mut app);
1609+
1610+
// app1 update
1611+
let update_proposal = app_update(&mut app);
1612+
camera_receive_update_proposal(&mut camera, update_proposal);
1613+
1614+
// app2 update
1615+
// NOTE: diagnosis: update generated by app2 is on an old epoch.
1616+
// This is currently prevented by fetching config response before generating a new heartbeat.
1617+
// Can there be a race condition there? What if config response hasn't been submitted yet?
1618+
let update_proposal = app_update(&mut app2);
1619+
1620+
pair_with_app3_inform_app2(&mut app2, psk_proposal_vec, commit_msg_vec, update_proposals);
1621+
camera_receive_update_proposal_ignore_old(&mut camera, update_proposal);
1622+
1623+
// app3 update
1624+
let update_proposal = app_update(&mut app3);
1625+
camera_receive_update_proposal(&mut camera, update_proposal);
1626+
1627+
// Create input video file to be encrypted (all 0's)
1628+
let video_pathname = "test_data/video_file";
1629+
let file_size: usize = 96 * 1024 + 135;
1630+
1631+
generate_dummy_file(video_pathname, file_size);
1632+
1633+
// Camera encrypts video file
1634+
let enc_video_pathname = "test_data/enc_video_file";
1635+
1636+
encrypt_video_file(
1637+
&mut camera,
1638+
video_pathname,
1639+
enc_video_pathname,
1640+
0,
1641+
).unwrap();
1642+
1643+
decrypt_and_check_file(&mut app, "app", enc_video_pathname, file_size);
1644+
decrypt_and_check_file(&mut app2, "app2", enc_video_pathname, file_size);
1645+
decrypt_and_check_file(&mut app3, "app3", enc_video_pathname, file_size);
1646+
}
14591647
}

0 commit comments

Comments
 (0)