Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-worker/src/input_mounts.rs
Line
Count
Source
1
// Copyright 2026 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use std::collections::HashSet;
16
use std::path::{Component, Path, PathBuf};
17
use std::sync::Arc;
18
19
use nativelink_error::{Code, Error, ResultExt, make_err};
20
use nativelink_proto::build::bazel::remote::execution::v2::{Command, Directory};
21
use nativelink_store::ac_utils::get_and_decode_digest;
22
use nativelink_store::fast_slow_store::FastSlowStore;
23
use nativelink_util::common::DigestInfo;
24
use parking_lot::Mutex;
25
26
use crate::directory_cache::{DigestLease, DirectoryCache, PreparedDirectory};
27
28
/// Selected immutable input subtrees and their cache pins for one action.
29
/// Pins remain held through cleanup, including after cancelled preparation.
30
#[derive(Debug)]
31
pub struct InputMounts {
32
    cache: Arc<DirectoryCache>,
33
    targets: HashSet<PathBuf>,
34
    #[cfg(target_os = "linux")]
35
    root: PathBuf,
36
    prepared: Mutex<Vec<(PathBuf, PreparedDirectory)>>,
37
}
38
39
/// Validates that mount paths are canonical, nonempty, and non-overlapping.
40
/// Paths are relative to the action input root. Mounting the whole root would
41
/// prevent the action from creating outputs.
42
95
pub fn validate_paths(paths: &[String]) -> Result<(), Error> {
43
95
    for (
i17
,
path17
) in paths.iter().enumerate() {
44
17
        if !is_canonical_relative(path)
45
8
            || paths[..i]
46
8
                .iter()
47
8
                .any(|other| 
paths_overlap3
(
Path::new(path)3
,
Path::new(other)3
))
48
        {
49
11
            return Err(make_err!(
50
11
                Code::InvalidArgument,
51
11
                "Read-only input mount paths must be nonempty, canonical, non-overlapping relative paths: {path:?}"
52
11
            ));
53
6
        }
54
    }
55
84
    Ok(())
56
95
}
57
58
/// Checks only the ancestors of selected paths, without walking their contents.
59
/// Actions without a matching directory should keep using the ordinary cache.
60
/// `paths` must have passed `validate_paths`.
61
12
pub async fn has_mountable_inputs(
62
12
    cas_store: &FastSlowStore,
63
12
    root_digest: DigestInfo,
64
12
    paths: &[String],
65
12
    lease: Option<&dyn DigestLease>,
66
12
) -> Result<bool, Error> {
67
13
    'target: for path in 
paths12
{
68
13
        let mut digest = root_digest;
69
15
        for component in 
path.split('/')13
{
70
15
            if let Some(
lease7
) = lease {
71
7
                lease.acquire(&digest);
72
8
            }
73
15
            let directory = get_and_decode_digest::<Directory>(cas_store, digest.into()).await
?0
;
74
15
            let Some(
child12
) = directory
75
15
                .directories
76
15
                .iter()
77
26
                .
find15
(|child| child.name == component)
78
            else {
79
3
                continue 'target;
80
            };
81
12
            digest = child
82
12
                .digest
83
12
                .as_ref()
84
12
                .err_tip(|| "Missing input directory digest")
?0
85
12
                .try_into()
?0
;
86
        }
87
10
        return Ok(true);
88
    }
89
2
    Ok(false)
90
12
}
91
92
26
fn is_canonical_relative(path: &str) -> bool {
93
26
    !path.is_empty()
94
25
        && !path.contains(['\\', '\0'])
95
39
        && 
path.split('/')23
.
all23
(|part| !
matches!6
(part, "" |
"."36
|
".."35
))
96
26
}
97
98
// Outputs are relative to the command's working directory and may use `..`
99
// (for example Chromium's ../clang-crashreports). Normalize lexically within
100
// the input root before checking overlap. Symlink aliases remain a workload
101
// constraint; resolving them would require walking the input tree on every hit.
102
23
fn resolve_output(cwd: &str, output: &str) -> Option<PathBuf> {
103
23
    if output.contains(['\\', '\0']) {
104
0
        return None;
105
23
    }
106
23
    let mut path = PathBuf::from(cwd);
107
40
    for component in 
Path::new(output)23
.
components23
() {
108
40
        match component {
109
29
            Component::Normal(part) => path.push(part),
110
2
            Component::CurDir => {}
111
            Component::ParentDir => {
112
6
                if !path.pop() {
113
1
                    return None;
114
5
                }
115
            }
116
3
            Component::RootDir | Component::Prefix(_) => return None,
117
        }
118
    }
119
19
    Some(path)
120
23
}
121
122
19
fn paths_overlap(a: &Path, b: &Path) -> bool {
123
19
    a.starts_with(b) || 
b12
.
starts_with12
(
a12
)
124
19
}
125
126
impl InputMounts {
127
    /// Creates mount state for a compatible command, or returns `None` to use
128
    /// ordinary materialization. Declared outputs must not overlap a selected
129
    /// subtree: the output collector cannot see the child's private mounts.
130
18
    pub fn for_command(
131
18
        cache: Arc<DirectoryCache>,
132
18
        paths: &[String],
133
18
        root: &Path,
134
18
        command: &Command,
135
18
    ) -> Option<Arc<Self>> {
136
18
        let cwd = &command.working_directory;
137
18
        if !cwd.is_empty() && 
!is_canonical_relative(cwd)9
{
138
0
            return None;
139
18
        }
140
18
        if paths.iter().any(|path| Path::new(cwd).starts_with(path)) {
141
1
            return None;
142
17
        }
143
18
        for output in command
144
17
            .output_paths
145
17
            .iter()
146
17
            .chain(&command.output_files)
147
17
            .chain(&command.output_directories)
148
        {
149
            // Empty output paths designate the whole working directory.
150
18
            let 
output16
= resolve_output(cwd, output)
?2
;
151
16
            if paths
152
16
                .iter()
153
16
                .any(|path| paths_overlap(&output, Path::new(path)))
154
            {
155
7
                return None;
156
9
            }
157
        }
158
8
        Some(Arc::new(Self {
159
8
            cache,
160
8
            targets: paths.iter().map(|path| root.join(path)).collect(),
161
            #[cfg(target_os = "linux")]
162
8
            root: root.to_owned(),
163
8
            prepared: Mutex::new(Vec::new()),
164
        }))
165
18
    }
166
167
    /// Prepares and pins a selected subtree before the input walk descends into it.
168
    /// Returns `true` when the caller should leave an empty mount point instead
169
    /// of materializing the subtree, or `false` when the path is not selected.
170
21
    pub async fn prepare(
171
21
        &self,
172
21
        path: &Path,
173
21
        digest: DigestInfo,
174
21
        lease: Option<&dyn DigestLease>,
175
21
    ) -> Result<bool, Error> {
176
21
        if !self.targets.contains(path) {
177
14
            return Ok(false);
178
7
        }
179
7
        let 
directory6
= self.cache.prepare_for_mount(digest, lease).await
?1
;
180
6
        self.prepared.lock().push((path.to_owned(), directory));
181
6
        Ok(true)
182
21
    }
183
184
    /// Resolve a program in a prepared mount before the child namespace exists.
185
    /// Validate symlinks against the pinned source, then return its path in the
186
    /// child workspace, not the cache path hidden by namespace setup.
187
    #[cfg(target_os = "linux")]
188
5
    pub fn executable_path(
189
5
        &self,
190
5
        cwd: &str,
191
5
        program: &std::ffi::OsStr,
192
5
    ) -> Result<Option<PathBuf>, Error> {
193
5
        let Some(program) = program.to_str() else {
194
0
            return Ok(None);
195
        };
196
5
        let Some(
relative3
) = resolve_output(cwd, program) else {
197
2
            return Ok(None);
198
        };
199
3
        let candidate = self.root.join(relative);
200
3
        for (target, directory) in self.prepared.lock().iter() {
201
3
            let Ok(suffix) = candidate.strip_prefix(target) else {
202
0
                continue;
203
            };
204
3
            let source = directory.path().canonicalize()
?0
;
205
3
            let executable = source.join(suffix).canonicalize().err_tip(|| 
{0
206
0
                format!(
207
                    "Could not resolve executable in read-only input mount: {}",
208
0
                    candidate.display()
209
                )
210
0
            })?;
211
3
            let 
relative2
= executable.strip_prefix(&source).map_err(|_|
{1
212
1
                make_err!(
213
1
                    Code::InvalidArgument,
214
                    "Executable symlink escapes its read-only input mount: {}",
215
1
                    candidate.display()
216
                )
217
1
            })?;
218
2
            return Ok(Some(target.join(relative)));
219
        }
220
0
        Ok(None)
221
5
    }
222
223
    /// Allocates mount paths and reads mount flags before entering pre-exec.
224
    #[cfg(target_os = "linux")]
225
4
    pub fn bind_mounts(&self) -> Result<Vec<crate::namespace_utils::ReadOnlyBindMount>, Error> {
226
4
        self.prepared
227
4
            .lock()
228
4
            .iter()
229
4
            .map(|(target, directory)| {
230
4
                crate::namespace_utils::ReadOnlyBindMount::new(directory.path(), target)
231
4
                    .map_err(Error::from)
232
4
            })
233
4
            .collect()
234
4
    }
235
}