Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 117 additions & 15 deletions crates/native-sidecar/src/execution/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -813,24 +813,96 @@ fn sync_vm_shadow_root_to_kernel(vm: &mut VmState) -> Result<(), SidecarError> {
}
}

let failed_replacements = propagate_shadow_deletions_to_kernel(vm, &mut snapshot.entries);
let kernel_owned = kernel_owned_shadow_paths(vm, &snapshot);
let failed_replacements = propagate_shadow_deletions_to_kernel(vm, &mut snapshot);
let mut synced_file_times = BTreeMap::new();
sync_host_directory_tree_to_kernel_inner(
vm,
&shadow_root,
&shadow_root,
"/",
&mut synced_file_times,
Some(&snapshot.entries),
Some(&failed_replacements),
Some(&ShadowSyncPlan {
inventory: &snapshot.entries,
failed_replacements: &failed_replacements,
kernel_owned: &kernel_owned,
}),
)?;
for (path, entry) in &mut snapshot.entries {
if entry.deletion_pending {
continue;
}
if let Some(shadow) = snapshot.shadow_stamps.get(path) {
entry.baseline = Some(ShadowSyncBaseline {
shadow: Some(*shadow),
kernel: kernel_shadow_sync_stamp(vm, path),
});
}
}
vm.shadow_sync_inventory = snapshot.entries;
Ok(())
}

/// Every guest runtime writes the kernel VFS directly, and most of those
/// writes never reach the shadow. A shadow change therefore applies to the
/// kernel only when the kernel node is unchanged since the last
/// reconciliation; otherwise the kernel holds the newer guest edit.
fn shadow_change_applies_to_kernel(
previous: Option<&ShadowSyncInventoryEntry>,
shadow: Option<ShadowSyncStamp>,
kernel: Option<ShadowSyncStamp>,
) -> bool {
match previous.map(|entry| entry.baseline) {
// A shadow node that was never inventoried may only fill a kernel gap.
None => kernel.is_none(),
// Seeded shadow content that was never reconciled is authoritative.
Some(None) => true,
Some(Some(baseline)) => baseline.shadow != shadow && baseline.kernel == kernel,
}
}

/// Shadow paths whose kernel node must not be overwritten by this walk.
fn kernel_owned_shadow_paths(vm: &mut VmState, snapshot: &ShadowSyncSnapshot) -> BTreeSet<String> {
let mut kernel_owned = BTreeSet::new();
for path in snapshot.entries.keys() {
let kernel = kernel_shadow_sync_stamp(vm, path);
if !shadow_change_applies_to_kernel(
vm.shadow_sync_inventory.get(path),
snapshot.shadow_stamps.get(path).copied(),
kernel,
) {
kernel_owned.insert(path.clone());
}
}
kernel_owned
}

/// Trusted bookkeeping read of a kernel node. It must not require the guest
/// `fs.stat` permission that a `KernelVm::lstat` would check.
fn kernel_shadow_sync_stamp(vm: &mut VmState, guest_path: &str) -> Option<ShadowSyncStamp> {
use agentos_kernel::vfs::VirtualFileSystem;
vm.kernel
.filesystem_mut()
.inner_mut()
.lstat(guest_path)
.ok()
.map(|stat| ShadowSyncStamp::kernel(&stat))
}

/// Paths the additive shadow walk must leave alone.
struct ShadowSyncPlan<'a> {
/// The entry appeared after the inventory pass when it is missing here.
inventory: &'a BTreeMap<String, ShadowSyncInventoryEntry>,
/// Stale kernel objects whose removal failed.
failed_replacements: &'a BTreeSet<String>,
/// Kernel nodes that changed since the last reconciliation.
kernel_owned: &'a BTreeSet<String>,
}

#[derive(Default)]
struct ShadowSyncSnapshot {
entries: BTreeMap<String, ShadowSyncInventoryEntry>,
shadow_stamps: BTreeMap<String, ShadowSyncStamp>,
incomplete_subtrees: BTreeSet<String>,
}

Expand Down Expand Up @@ -922,10 +994,23 @@ fn collect_shadow_sync_snapshot_inner(
} else {
continue;
};
let metadata = match entry.metadata() {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(SidecarError::Io(format!(
"failed to inventory host shadow entry {}: {error}",
host_path.display()
)));
}
};
snapshot.entries.insert(
guest_path.clone(),
ShadowSyncInventoryEntry::present(node_type),
);
snapshot
.shadow_stamps
.insert(guest_path.clone(), ShadowSyncStamp::host(&metadata));
if node_type == ShadowNodeType::Directory {
collect_shadow_sync_snapshot_inner(shadow_root, &host_path, &guest_path, snapshot)?;
}
Expand All @@ -942,8 +1027,9 @@ fn collect_shadow_sync_snapshot_inner(
/// leaves before the directories that contain them.
fn propagate_shadow_deletions_to_kernel(
vm: &mut VmState,
current: &mut BTreeMap<String, ShadowSyncInventoryEntry>,
snapshot: &mut ShadowSyncSnapshot,
) -> BTreeSet<String> {
let current = &mut snapshot.entries;
let stale = vm
.shadow_sync_inventory
.iter()
Expand All @@ -962,6 +1048,16 @@ fn propagate_shadow_deletions_to_kernel(
{
continue;
}
let kernel = kernel_shadow_sync_stamp(vm, &path);
if !shadow_change_applies_to_kernel(
Some(&previous),
snapshot.shadow_stamps.get(&path).copied(),
kernel,
) {
// The kernel node was replaced or edited after the shadow copy
// went stale (for example `rm f && echo new > f` in a guest).
continue;
}
let stat = match vm.kernel.lstat(&path) {
Ok(stat) => stat,
Err(error) if error.code() == "ENOENT" => continue,
Expand Down Expand Up @@ -1003,10 +1099,13 @@ fn retain_shadow_deletion_tombstone(
) {
current
.entry(path.to_owned())
.and_modify(|entry| entry.deletion_pending = true)
.and_modify(|entry| {
entry.deletion_pending = true;
entry.baseline = previous.baseline;
})
.or_insert(ShadowSyncInventoryEntry {
node_type: previous.node_type,
deletion_pending: true,
..previous
});
}

Expand Down Expand Up @@ -1115,7 +1214,6 @@ fn sync_host_directory_tree_to_kernel(
&normalized_guest_root,
&mut synced_file_times,
None,
None,
)
}

Expand All @@ -1134,8 +1232,7 @@ fn sync_host_directory_tree_to_kernel_inner(
current_host_dir: &Path,
guest_root: &str,
synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>,
expected_inventory: Option<&BTreeMap<String, ShadowSyncInventoryEntry>>,
failed_replacements: Option<&BTreeSet<String>>,
plan: Option<&ShadowSyncPlan<'_>>,
) -> Result<(), SidecarError> {
let entries = match fs::read_dir(current_host_dir) {
Ok(entries) => entries,
Expand Down Expand Up @@ -1196,26 +1293,29 @@ fn sync_host_directory_tree_to_kernel_inner(
if should_skip_shadow_sync_path(vm, &guest_path) {
continue;
}
if expected_inventory.is_some_and(|inventory| !inventory.contains_key(&guest_path)) {
if plan.is_some_and(|plan| !plan.inventory.contains_key(&guest_path)) {
// The entry appeared after the inventory pass. Pick it up on the
// next sync rather than mixing two different shadow snapshots.
continue;
}
if failed_replacements.is_some_and(|failed| failed.contains(&guest_path)) {
if plan.is_some_and(|plan| plan.failed_replacements.contains(&guest_path)) {
// Never write through a stale object whose removal failed. The
// retained tombstone will retry before a future additive write.
continue;
}
let kernel_owned = plan.is_some_and(|plan| plan.kernel_owned.contains(&guest_path));

if file_type.is_dir() {
ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::Directory)?;
if !kernel_owned {
ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::Directory)?;
}
let metadata = entry.metadata().map_err(|error| {
SidecarError::Io(format!(
"failed to read host shadow metadata {}: {error}",
host_path.display()
))
})?;
if !is_shadow_bootstrap_dir(&guest_path) {
if !kernel_owned && !is_shadow_bootstrap_dir(&guest_path) {
if !vm.kernel.exists(&guest_path).unwrap_or(false) {
vm.kernel.mkdir(&guest_path, true).map_err(|error| {
SidecarError::InvalidState(format!(
Expand Down Expand Up @@ -1243,11 +1343,13 @@ fn sync_host_directory_tree_to_kernel_inner(
&host_path,
guest_root,
synced_file_times,
expected_inventory,
failed_replacements,
plan,
)?;
continue;
}
if kernel_owned {
continue;
}

if file_type.is_file() {
ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::File)?;
Expand Down
6 changes: 3 additions & 3 deletions crates/native-sidecar/src/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ use crate::state::{
PendingTcpSocket, PendingUnixConnectionGuard, PendingUnixSocket, PlainSocketWritePayload,
ProcNetEntry, ProcessEventEnvelope, PythonHostSocket, PythonSocketConnectCompletion,
PythonTcpReadBuffer, QueuedHttp2Command, QueuedHttp2Event, ReactorIoLimits,
ResolvedChildProcessExecution, ResolvedTcpConnectAddr, ShadowNodeType,
ShadowSyncInventoryEntry, SharedBridge, SharedSidecarRequestClient, SidecarKernel,
SocketDescriptionLease, SocketQueryKind, SocketReadinessRegistration,
ResolvedChildProcessExecution, ResolvedTcpConnectAddr, ShadowNodeType, ShadowSyncBaseline,
ShadowSyncInventoryEntry, ShadowSyncStamp, SharedBridge, SharedSidecarRequestClient,
SidecarKernel, SocketDescriptionLease, SocketQueryKind, SocketReadinessRegistration,
SocketReadinessSubscribers, TlsWritePayload, VmDnsConfig, VmFetchBodyMode, VmFetchStreamState,
VmHandle, VmListenPolicy, VmPendingByteBudget, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG,
EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV, HOST_FUNCTION_DRIVER_NAME,
Expand Down
72 changes: 48 additions & 24 deletions crates/native-sidecar/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ use crate::service::{
python_error,
};
use crate::state::{
ActiveExecutionEvent, ActiveProcess, BridgeError, ShadowNodeType, ShadowSyncInventoryEntry,
SharedBridge, SidecarKernel, VmHandle, VmState, EXECUTION_DRIVER_NAME,
PYTHON_VFS_RPC_GUEST_ROOT,
ActiveExecutionEvent, ActiveProcess, BridgeError, ShadowNodeType, ShadowSyncBaseline,
ShadowSyncInventoryEntry, ShadowSyncStamp, SharedBridge, SidecarKernel, VmHandle, VmState,
EXECUTION_DRIVER_NAME, PYTHON_VFS_RPC_GUEST_ROOT,
};
use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError};

Expand Down Expand Up @@ -736,23 +736,20 @@ fn refresh_shadow_inventory_path(vm: &mut VmState, guest_path: &str) -> Result<(
}
let guest_path = normalize_path(guest_path);
let mut updates = collect_shadow_inventory_ancestors(vm, &guest_path)?;
let Some(node_type) = shadow_inventory_kernel_node_type(vm, &guest_path)? else {
let Some(entry) = shadow_inventory_entry(vm, &guest_path)? else {
forget_shadow_inventory_path(vm, &guest_path);
return Ok(());
};
updates.insert(
guest_path.clone(),
ShadowSyncInventoryEntry::present(node_type),
);
if node_type == ShadowNodeType::Directory {
updates.insert(guest_path.clone(), entry);
if entry.node_type == ShadowNodeType::Directory {
let entries = vm
.kernel
.read_dir_recursive(&guest_path, None)
.map_err(kernel_error)?;
for entry in entries {
let path = normalize_path(&entry.path);
if let Some(node_type) = shadow_inventory_kernel_node_type(vm, &path)? {
updates.insert(path, ShadowSyncInventoryEntry::present(node_type));
if let Some(entry) = shadow_inventory_entry(vm, &path)? {
updates.insert(path, entry);
}
}
}
Expand All @@ -772,11 +769,8 @@ fn refresh_shadow_inventory_path(vm: &mut VmState, guest_path: &str) -> Result<(
fn refresh_shadow_inventory_node(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> {
let guest_path = normalize_path(guest_path);
let mut updates = collect_shadow_inventory_ancestors(vm, &guest_path)?;
if let Some(node_type) = shadow_inventory_kernel_node_type(vm, &guest_path)? {
updates.insert(
guest_path.clone(),
ShadowSyncInventoryEntry::present(node_type),
);
if let Some(entry) = shadow_inventory_entry(vm, &guest_path)? {
updates.insert(guest_path.clone(), entry);
}
vm.shadow_sync_inventory.extend(updates);
Ok(())
Expand All @@ -801,20 +795,21 @@ fn collect_shadow_inventory_ancestors(
}
let mut updates = BTreeMap::new();
for ancestor in ancestors.into_iter().rev() {
if shadow_inventory_kernel_node_type(vm, &ancestor)? == Some(ShadowNodeType::Directory) {
updates.insert(
ancestor,
ShadowSyncInventoryEntry::present(ShadowNodeType::Directory),
);
if let Some(entry) = shadow_inventory_entry(vm, &ancestor)? {
if entry.node_type == ShadowNodeType::Directory {
updates.insert(ancestor, entry);
}
}
}
Ok(updates)
}

fn shadow_inventory_kernel_node_type(
/// Inventory a path whose kernel state the sidecar just mirrored into the
/// shadow, recording both sides as reconciled.
fn shadow_inventory_entry(
vm: &mut VmState,
guest_path: &str,
) -> Result<Option<ShadowNodeType>, SidecarError> {
) -> Result<Option<ShadowSyncInventoryEntry>, SidecarError> {
// This inventory is trusted sidecar bookkeeping after the guest mutation has
// already passed its operation-specific permission check. Re-entering through
// `KernelVm::lstat` would incorrectly require a separate guest `fs.stat`
Expand All @@ -831,7 +826,24 @@ fn shadow_inventory_kernel_node_type(
} else {
ShadowNodeType::File
};
Ok(Some(node_type))
let shadow_path = shadow_host_path_for_guest(&vm.cwd, guest_path);
let shadow = match fs::symlink_metadata(&shadow_path) {
Ok(metadata) => Some(ShadowSyncStamp::host(&metadata)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => {
return Err(SidecarError::Io(format!(
"failed to inspect shadow path {}: {error}",
shadow_path.display()
)));
}
};
Ok(Some(ShadowSyncInventoryEntry::reconciled(
node_type,
ShadowSyncBaseline {
shadow,
kernel: Some(ShadowSyncStamp::kernel(&stat)),
},
)))
}

fn forget_shadow_inventory_path(vm: &mut VmState, guest_path: &str) {
Expand Down Expand Up @@ -5377,8 +5389,20 @@ fn sync_active_shadow_path_to_kernel(
if host_paths.is_empty() && !vm.kernel.exists(&guest_path).unwrap_or(false) {
host_paths.push(shadow_host_path_for_guest(&vm.cwd, &guest_path));
}
// The root walk above already decided which side wins for a reconciled
// shadow path; importing it again here would undo a newer kernel edit.
let reconciled_in_root_shadow = vm
.shadow_sync_inventory
.get(&guest_path)
.is_some_and(|entry| entry.baseline.is_some());
let vm_shadow_root = normalize_host_path(&vm.cwd);

for host_path in host_paths {
if reconciled_in_root_shadow
&& path_is_within_root(&normalize_host_path(&host_path), &vm_shadow_root)
{
continue;
}
let metadata = match fs::symlink_metadata(&host_path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Expand Down
Loading
Loading