@@ -212,7 +212,7 @@ func (e *ExecOp) CacheMap(ctx context.Context, g session.Group, index int) (*sol
212212 }
213213 cm .Deps [i ].Selector = digest .FromBytes (bytes .Join (dgsts , []byte {0 }))
214214 }
215- if ! dep .NoContentBasedHash {
215+ if dep .ContentBasedHash {
216216 cm .Deps [i ].ComputeDigestFunc = opsutils .NewContentHashFunc (toSelectors (dedupePaths (dep .Selectors )))
217217 }
218218 cm .Deps [i ].PreprocessFunc = unlazyResultFunc
@@ -275,8 +275,11 @@ func toSelectors(p []string) []opsutils.Selector {
275275}
276276
277277type dep struct {
278- Selectors []string
279- NoContentBasedHash bool
278+ Selectors []string
279+
280+ // ContentBasedHash enables content-based caching. This is used to ensure
281+ // that all caching is done safely and efficiently.
282+ ContentBasedHash bool
280283}
281284
282285func (e * ExecOp ) getMountDeps () ([]dep , error ) {
@@ -292,9 +295,53 @@ func (e *ExecOp) getMountDeps() ([]dep, error) {
292295 sel := path .Join ("/" , m .Selector )
293296 deps [m .Input ].Selectors = append (deps [m .Input ].Selectors , sel )
294297
295- if (! m .Readonly || m .Dest == pb .RootMount ) && m .Output != - 1 { // exclude read-only rootfs && read-write mounts
296- deps [m .Input ].NoContentBasedHash = true
298+ // Assume that we *cannot* perform content-based caching, and then
299+ // enable it selectively only for cases where we want to
300+ contentBasedCache := false
301+
302+ // Allow content-based cached where safe - these are enforced to avoid
303+ // the following case:
304+ // - A "snapshot" contains "foo/a.txt" and "bar/b.txt"
305+ // - "RUN --mount from=snapshot,src=bar touch bar/c.txt" creates a new
306+ // file in bar
307+ // - If we run again, but this time "snapshot" contains a new
308+ // "foo/sneaky.txt", the content-based cache matches the previous
309+ // run, since we only select "bar"
310+ // - But this cached result is incorrect - "foo/sneaky.txt" isn't in
311+ // our cached result, but it is in our input.
312+ if m .Output == pb .SkipOutput {
313+ // if the mount has no outputs, it's safe to enable content-based
314+ // caching, since it's guaranteed to not be used as an input for
315+ // any future steps
316+ contentBasedCache = true
317+ } else if m .Readonly {
318+ // if the mount is read-only, then it's also safe, since it can't
319+ // be modified by the operation
320+ contentBasedCache = true
321+ } else if sel == pb .RootMount {
322+ // if the mount mounts the entire source, then it's also safe,
323+ // since there are no unselected "sneaky" files
324+ contentBasedCache = true
325+ }
326+
327+ // Now apply the user-specified option.
328+ switch m .ContentCache {
329+ case pb .MountContentCache_OFF :
330+ contentBasedCache = false
331+ case pb .MountContentCache_ON :
332+ if ! contentBasedCache {
333+ // If we can't enable cache for safety, then force-enabling it is invalid
334+ return nil , errors .Errorf ("invalid mount cache content %v" , m )
335+ }
336+ case pb .MountContentCache_DEFAULT :
337+ if m .Dest == pb .RootMount {
338+ // we explicitly choose to not implement it on the root mount,
339+ // since this is likely very expensive (and not incredibly useful)
340+ contentBasedCache = false
341+ }
297342 }
343+
344+ deps [m .Input ].ContentBasedHash = contentBasedCache
298345 }
299346 return deps , nil
300347}
0 commit comments