/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 | | } |