Skip to content

Commit 9c0f350

Browse files
authored
Assure VirtualBranches based metadata can't be used in a racy fashion. (#9808)
Currently it's easy to create an instance the is used for writing before obtaining a workspace lock, which leads to race-condition if the metadata is changed after all. The new implementation makes this impossible by enforcing a permission to be provided.
2 parents 24f18ba + 7fee36d commit 9c0f350

File tree

4 files changed

+83
-28
lines changed

4 files changed

+83
-28
lines changed

crates/but-api/src/commands/diff.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ fn changes_in_branch_inner(
100100
remote: Option<String>,
101101
branch_name: String,
102102
) -> anyhow::Result<TreeChanges> {
103-
let (repo, _meta, graph) = ctx.graph_and_meta(ctx.gix_repo()?)?;
103+
let guard = ctx.project().shared_worktree_access();
104+
let (repo, _meta, graph) = ctx.graph_and_meta(ctx.gix_repo()?, guard.read_permission())?;
104105
let name = if let Some(remote) = remote {
105106
Category::RemoteBranch.to_full_name(format!("{remote}/{branch_name}").as_str())
106107
} else {

crates/but-api/src/commands/stack.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,14 @@ pub fn create_reference(app: &App, params: create_reference::Params) -> Result<(
8888
})
8989
.transpose()?;
9090

91-
let (repo, mut meta, graph) = ctx.graph_and_meta_and_repo()?;
91+
let mut guard = ctx.project().exclusive_worktree_access();
92+
let (repo, mut meta, graph) = ctx.graph_and_meta_mut_and_repo(guard.write_permission())?;
9293
_ = but_workspace::branch::create_reference(
9394
new_ref,
9495
anchor,
9596
&repo,
9697
&graph.to_workspace()?,
97-
&mut meta,
98+
&mut *meta,
9899
)?;
99100
Ok(())
100101
}
@@ -104,7 +105,8 @@ pub fn create_branch(app: &App, params: CreateBranchParams) -> Result<(), Error>
104105
let ctx = CommandContext::open(&project, app.app_settings.get()?.clone())?;
105106
if app.app_settings.get()?.feature_flags.ws3 {
106107
use ReferencePosition::Above;
107-
let (repo, mut meta, graph) = ctx.graph_and_meta_and_repo()?;
108+
let mut guard = project.exclusive_worktree_access();
109+
let (repo, mut meta, graph) = ctx.graph_and_meta_mut_and_repo(guard.write_permission())?;
108110
let ws = graph.to_workspace()?;
109111
let stack = ws.try_find_stack_by_id(params.stack_id)?;
110112
let new_ref = Category::LocalBranch
@@ -117,7 +119,6 @@ pub fn create_branch(app: &App, params: CreateBranchParams) -> Result<(), Error>
117119
.into());
118120
}
119121

120-
let mut guard = project.exclusive_worktree_access();
121122
ctx.snapshot_create_dependent_branch(&params.request.name, guard.write_permission())
122123
.ok();
123124
_ = but_workspace::branch::create_reference(
@@ -146,7 +147,7 @@ pub fn create_branch(app: &App, params: CreateBranchParams) -> Result<(), Error>
146147
},
147148
&repo,
148149
&ws,
149-
&mut meta,
150+
&mut *meta,
150151
)?;
151152
} else {
152153
// NOTE: locking is built-in here.
@@ -166,20 +167,20 @@ pub struct RemoveBranchParams {
166167
pub fn remove_branch(app: &App, params: RemoveBranchParams) -> Result<(), Error> {
167168
let project = gitbutler_project::get(params.project_id)?;
168169
let ctx = CommandContext::open(&project, app.app_settings.get()?.clone())?;
170+
let mut guard = project.exclusive_worktree_access();
169171
if app.app_settings.get()?.feature_flags.ws3 {
170-
let (repo, mut meta, graph) = ctx.graph_and_meta_and_repo()?;
172+
let (repo, mut meta, graph) = ctx.graph_and_meta_mut_and_repo(guard.write_permission())?;
171173
let ws = graph.to_workspace()?;
172174
let ref_name = Category::LocalBranch
173175
.to_full_name(params.branch_name.as_str())
174176
.map_err(anyhow::Error::from)?;
175-
let mut guard = project.exclusive_worktree_access();
176177
ctx.snapshot_remove_dependent_branch(&params.branch_name, guard.write_permission())
177178
.ok();
178179
but_workspace::branch::remove_reference(
179180
ref_name.as_ref(),
180181
&repo,
181182
&ws,
182-
&mut meta,
183+
&mut *meta,
183184
but_workspace::branch::remove_reference::Options {
184185
avoid_anonymous_stacks: true,
185186
// The UI kind of keeps it, but we can't do that somehow

crates/but-api/src/commands/virtual_branches.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ pub fn create_virtual_branch(
4343
let ws3_enabled = app.app_settings.get()?.feature_flags.ws3;
4444
let ctx = CommandContext::open(&project, app.app_settings.get()?.clone())?;
4545
let stack_entry = if ws3_enabled {
46-
let (repo, mut meta, graph) = ctx.graph_and_meta_and_repo()?;
46+
let mut guard = project.exclusive_worktree_access();
47+
let (repo, mut meta, graph) = ctx.graph_and_meta_mut_and_repo(guard.write_permission())?;
4748
let ws = graph.to_workspace()?;
4849
let new_ref = Category::LocalBranch
4950
.to_full_name(
@@ -63,9 +64,13 @@ pub fn create_virtual_branch(
6364
)
6465
.map_err(anyhow::Error::from)?;
6566

66-
let _guard = project.exclusive_worktree_access();
67-
let graph =
68-
but_workspace::branch::create_reference(new_ref.as_ref(), None, &repo, &ws, &mut meta)?;
67+
let graph = but_workspace::branch::create_reference(
68+
new_ref.as_ref(),
69+
None,
70+
&repo,
71+
&ws,
72+
&mut *meta,
73+
)?;
6974

7075
let ws = graph.to_workspace()?;
7176
let (stack_idx, segment_idx) = ws

crates/gitbutler-command-context/src/lib.rs

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use anyhow::Result;
2-
use but_graph::VirtualBranchesTomlMetadata;
32
use but_settings::AppSettings;
3+
use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission};
44
use gitbutler_project::Project;
5+
use std::ops::{Deref, DerefMut};
56
use std::path::Path;
67

78
pub struct CommandContext {
@@ -14,6 +15,42 @@ pub struct CommandContext {
1415
db_handle: Option<but_db::DbHandle>,
1516
}
1617

18+
/// A [`but_graph::VirtualBranchesTomlMetadata`] instance that is only accessible if it sees a read
19+
/// permission upon instantiation.
20+
///
21+
/// This is necessary only while the `virtual_branches.toml` file is used as it's read eagerly on
22+
/// instantiation, and must thus not interleave with other writers.
23+
pub struct VirtualBranchesTomlMetadata(but_graph::VirtualBranchesTomlMetadata);
24+
25+
impl Deref for VirtualBranchesTomlMetadata {
26+
type Target = but_graph::VirtualBranchesTomlMetadata;
27+
28+
fn deref(&self) -> &Self::Target {
29+
&self.0
30+
}
31+
}
32+
33+
/// A [`but_graph::VirtualBranchesTomlMetadata`] instance that is only accessible if it sees a write
34+
/// permission upon instantiation.
35+
///
36+
/// This is necessary only while the `virtual_branches.toml` file is used as it's read eagerly on
37+
/// instantiation, and must thus not interleave with other writers.
38+
pub struct VirtualBranchesTomlMetadataMut(but_graph::VirtualBranchesTomlMetadata);
39+
40+
impl Deref for VirtualBranchesTomlMetadataMut {
41+
type Target = but_graph::VirtualBranchesTomlMetadata;
42+
43+
fn deref(&self) -> &Self::Target {
44+
&self.0
45+
}
46+
}
47+
48+
impl DerefMut for VirtualBranchesTomlMetadataMut {
49+
fn deref_mut(&mut self) -> &mut Self::Target {
50+
&mut self.0
51+
}
52+
}
53+
1754
impl CommandContext {
1855
/// Open the repository identified by `project` and perform some checks.
1956
pub fn open(project: &Project, app_settings: AppSettings) -> Result<Self> {
@@ -29,10 +66,6 @@ impl CommandContext {
2966
pub fn project(&self) -> &Project {
3067
&self.project
3168
}
32-
//
33-
// pub fn meta(&self) -> Result<VirtualBranchesTomlMetadata> {
34-
// VirtualBranchesTomlMetadata::from_path(project.gb_dir().join("virtual_branches.toml"))
35-
// }
3669

3770
/// Return the [`project`](Self::project) repository.
3871
pub fn repo(&self) -> &git2::Repository {
@@ -65,38 +98,42 @@ impl CommandContext {
6598

6699
/// Create a new Graph traversal from the current HEAD, using (and returning) the given `repo` (configured by the caller),
67100
/// along with a new metadata instance, and the graph itself.
101+
///
102+
/// The read-permission is required to obtain a shared metadata instance. Note that it must be held
103+
/// for until the end of the operation for the protection to be effective.
68104
pub fn graph_and_meta(
69105
&self,
70106
repo: gix::Repository,
107+
_read_only: &WorktreeReadPermission,
71108
) -> Result<(
72109
gix::Repository,
73110
VirtualBranchesTomlMetadata,
74111
but_graph::Graph,
75112
)> {
76113
let meta = self.meta()?;
77114
let graph = but_graph::Graph::from_head(&repo, &meta, meta.graph_options())?;
78-
Ok((repo, meta, graph))
115+
Ok((repo, VirtualBranchesTomlMetadata(meta), graph))
79116
}
80117

81118
/// Open the repository with standard options and create a new Graph traversal from the current HEAD,
82119
/// along with a new metadata instance, and the graph itself.
83120
///
121+
/// The write-permission is required to obtain a mutable metadata instance. Note that it must be held
122+
/// for until the end of the operation for the protection to be effective.
123+
///
84124
/// Use [`Self::graph_and_meta()`] if control over the repository configuration is needed.
85-
pub fn graph_and_meta_and_repo(
125+
pub fn graph_and_meta_mut_and_repo(
86126
&self,
127+
_write: &mut WorktreeWritePermission,
87128
) -> Result<(
88129
gix::Repository,
89-
VirtualBranchesTomlMetadata,
130+
VirtualBranchesTomlMetadataMut,
90131
but_graph::Graph,
91132
)> {
92133
let repo = self.gix_repo()?;
93-
self.graph_and_meta(repo)
94-
}
95-
96-
/// Return the `RefMetadata` implementation based on the `virtual_branches.toml` file.
97-
/// This can one day be changed to auto-migrate away from the toml and to the database.
98-
pub fn meta(&self) -> Result<VirtualBranchesTomlMetadata> {
99-
VirtualBranchesTomlMetadata::from_path(self.project.gb_dir().join("virtual_branches.toml"))
134+
let meta = self.meta()?;
135+
let graph = but_graph::Graph::from_head(&repo, &meta, meta.graph_options())?;
136+
Ok((repo, VirtualBranchesTomlMetadataMut(meta), graph))
100137
}
101138

102139
/// Return a newly opened `gitoxide` repository, with all configuration available
@@ -133,6 +170,17 @@ impl CommandContext {
133170
}
134171
}
135172

173+
/// Keep these private
174+
impl CommandContext {
175+
/// Return the `RefMetadata` implementation based on the `virtual_branches.toml` file.
176+
/// This can one day be changed to auto-migrate away from the toml and to the database.
177+
fn meta(&self) -> Result<but_graph::VirtualBranchesTomlMetadata> {
178+
but_graph::VirtualBranchesTomlMetadata::from_path(
179+
self.project.gb_dir().join("virtual_branches.toml"),
180+
)
181+
}
182+
}
183+
136184
/// Return a newly opened `gitoxide` repository, with all configuration available
137185
/// to correctly figure out author and committer names (i.e. with most global configuration loaded),
138186
/// *and* which will perform diffs quickly thanks to an adequate object cache.

0 commit comments

Comments
 (0)