Skip to content

Commit 7760121

Browse files
committed
Check that digesting consumes the expected number of bytes.
In Bazel, the time a file is `stat`ed to find its size can be very far from the time the file's digest is computed, which creates a window for shenanigans. Allow passing the expected size into `Path.getDigest` and raise an error there if the number of digested bytes doesn't match the previously seen size.
1 parent 28ec988 commit 7760121

26 files changed

+165
-91
lines changed

src/main/java/com/google/devtools/build/lib/actions/FileArtifactValue.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ private static FileArtifactValue create(
273273
return new DirectoryArtifactValue(path.getLastModifiedTime());
274274
}
275275
if (digest == null) {
276-
digest = DigestUtils.getDigestWithManualFallback(path, xattrProvider);
276+
digest = DigestUtils.getDigestWithManualFallback(path, size, xattrProvider);
277277
}
278278
Preconditions.checkState(digest != null, path);
279279
return createForNormalFile(digest, proxy, size);

src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ public BundledFileSystem() {
126126

127127
@Override
128128
protected synchronized byte[] getFastDigest(PathFragment path) {
129-
return getDigest(path);
129+
return getDigest(path, -1);
130130
}
131131

132132
@Override
133-
protected synchronized byte[] getDigest(PathFragment path) {
133+
protected synchronized byte[] getDigest(PathFragment path, long expectedSize) {
134134
return getDigestFunction().getHashFunction().hashString(path.toString(), UTF_8).asBytes();
135135
}
136136
}

src/main/java/com/google/devtools/build/lib/exec/RunfilesTreeUpdater.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ private void updateRunfilesTree(
138138
if (tree.getSymlinksMode() != SKIP
139139
&& !outputManifest.isSymbolicLink()
140140
&& Arrays.equals(
141-
DigestUtils.getDigestWithManualFallback(outputManifest, xattrProvider),
142-
DigestUtils.getDigestWithManualFallback(inputManifest, xattrProvider))
141+
DigestUtils.getDigestWithManualFallbackWhenSizeUnknown(outputManifest, xattrProvider),
142+
DigestUtils.getDigestWithManualFallbackWhenSizeUnknown(inputManifest, xattrProvider))
143143
&& (OS.getCurrent() != OS.WINDOWS || isRunfilesDirectoryPopulated(runfilesDirPath))) {
144144
return;
145145
}

src/main/java/com/google/devtools/build/lib/exec/SpawnLogContext.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ protected Digest computeDigest(
177177
// Try to obtain a digest from the filesystem.
178178
return builder
179179
.setHash(
180-
HashCode.fromBytes(DigestUtils.getDigestWithManualFallback(path, xattrProvider))
180+
HashCode.fromBytes(
181+
DigestUtils.getDigestWithManualFallback(path, fileSize, xattrProvider))
181182
.toString())
182183
.setSizeBytes(fileSize)
183184
.build();

src/main/java/com/google/devtools/build/lib/remote/RemoteActionFileSystem.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ protected byte[] getFastDigest(PathFragment path) throws IOException {
434434
}
435435

436436
@Override
437-
protected byte[] getDigest(PathFragment path) throws IOException {
437+
protected byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
438438
path = resolveSymbolicLinks(path).asFragment();
439439
// Try to obtain a fast digest through a stat. This is only possible for in-memory files.
440440
// The parent path has already been canonicalized by resolveSymbolicLinks, so FOLLOW_NONE is
@@ -443,7 +443,7 @@ protected byte[] getDigest(PathFragment path) throws IOException {
443443
if (status instanceof FileStatusWithDigest) {
444444
return ((FileStatusWithDigest) status).getDigest();
445445
}
446-
return localFs.getPath(path).getDigest();
446+
return localFs.getPath(path).getDigest(expectedSize);
447447
}
448448

449449
@Override

src/main/java/com/google/devtools/build/lib/remote/util/DigestUtil.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ public Digest compute(Path file) throws IOException {
7272
}
7373

7474
public Digest compute(Path file, long fileSize) throws IOException {
75-
return buildDigest(DigestUtils.getDigestWithManualFallback(file, xattrProvider), fileSize);
75+
return buildDigest(
76+
DigestUtils.getDigestWithManualFallback(file, fileSize, xattrProvider), fileSize);
7677
}
7778

7879
public Digest compute(VirtualActionInput input) throws IOException {

src/main/java/com/google/devtools/build/lib/skyframe/ActionOutputMetadataStore.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ private FileArtifactValue constructFileArtifactValue(
557557
// possible to hit the digest cache - we probably already computed the digest for the
558558
// target during previous action execution.
559559
Path pathToDigest = isResolvedSymlink ? statAndValue.realPath() : statAndValue.pathNoFollow();
560-
actualDigest = DigestUtils.manuallyComputeDigest(pathToDigest);
560+
actualDigest = DigestUtils.manuallyComputeDigest(pathToDigest, value.getSize());
561561
}
562562

563563
if (!isResolvedSymlink) {

src/main/java/com/google/devtools/build/lib/testing/vfs/SpiedFileSystem.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ public boolean createWritableDirectory(PathFragment path) throws IOException {
5858
}
5959

6060
@Override
61-
public byte[] getDigest(PathFragment path) throws IOException {
62-
return super.getDigest(path);
61+
public byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
62+
return super.getDigest(path, expectedSize);
6363
}
6464

6565
@Override

src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -446,11 +446,11 @@ protected byte[] getFastDigest(PathFragment path) throws IOException {
446446
}
447447

448448
@Override
449-
protected byte[] getDigest(PathFragment path) throws IOException {
449+
protected byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
450450
String name = path.toString();
451451
long startTime = Profiler.nanoTimeMaybe();
452452
try {
453-
return super.getDigest(path);
453+
return super.getDigest(path, expectedSize);
454454
} finally {
455455
profiler.logSimpleTask(startTime, ProfilerTask.VFS_MD5, name);
456456
}

src/main/java/com/google/devtools/build/lib/vfs/DigestUtils.java

+27-6
Original file line numberDiff line numberDiff line change
@@ -160,19 +160,40 @@ public static CacheStats getCacheStats() {
160160
* #manuallyComputeDigest} to skip an additional attempt to obtain the fast digest.
161161
*
162162
* @param path Path of the file.
163+
* @param fileSize Size of the file. Used to determine if digest calculation should be done
164+
* serially or in parallel. Files larger than a certain threshold will be read serially, in
165+
* order to avoid excessive disk seeks.
163166
*/
164-
public static byte[] getDigestWithManualFallback(Path path, XattrProvider xattrProvider)
165-
throws IOException {
167+
public static byte[] getDigestWithManualFallback(
168+
Path path, long fileSize, XattrProvider xattrProvider) throws IOException {
166169
byte[] digest = xattrProvider.getFastDigest(path);
167-
return digest != null ? digest : manuallyComputeDigest(path);
170+
return digest != null ? digest : manuallyComputeDigest(path, fileSize);
171+
}
172+
173+
/**
174+
* Gets the digest of {@code path}, using a constant-time xattr call if the filesystem supports
175+
* it, and calculating the digest manually otherwise.
176+
*
177+
* <p>Unlike {@link #getDigestWithManualFallback}, will not rate-limit manual digesting of files,
178+
* so only use this method if the file size is truly unknown and you don't expect many concurrent
179+
* manual digests of large files.
180+
*
181+
* @param path Path of the file.
182+
*/
183+
public static byte[] getDigestWithManualFallbackWhenSizeUnknown(
184+
Path path, XattrProvider xattrProvider) throws IOException {
185+
return getDigestWithManualFallback(path, -1, xattrProvider);
168186
}
169187

170188
/**
171189
* Calculates the digest manually.
172190
*
173191
* @param path Path of the file.
192+
* @param fileSize Size of the file. Used to determine if digest calculation should be done
193+
* serially or in parallel. Files larger than a certain threshold will be read serially, in
194+
* order to avoid excessive disk seeks.
174195
*/
175-
public static byte[] manuallyComputeDigest(Path path) throws IOException {
196+
public static byte[] manuallyComputeDigest(Path path, long fileSize) throws IOException {
176197
byte[] digest;
177198

178199
// Attempt a cache lookup if the cache is enabled.
@@ -186,9 +207,9 @@ public static byte[] manuallyComputeDigest(Path path) throws IOException {
186207
}
187208
}
188209

189-
digest = path.getDigest();
210+
digest = path.getDigest(fileSize);
190211

191-
Preconditions.checkNotNull(digest, "Missing digest for %s", path);
212+
Preconditions.checkNotNull(digest, "Missing digest for %s (size %s)", path, fileSize);
192213
if (cache != null) {
193214
cache.put(key, digest);
194215
}

src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java

+22-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.google.common.base.Preconditions;
2222
import com.google.common.collect.Lists;
23+
import com.google.common.hash.Funnels;
2324
import com.google.common.io.ByteSource;
2425
import com.google.common.io.CharStreams;
2526
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
@@ -33,6 +34,7 @@
3334
import java.util.Collection;
3435
import java.util.Iterator;
3536
import java.util.List;
37+
import java.util.Locale;
3638
import javax.annotation.Nullable;
3739

3840
/** This interface models a file system. */
@@ -347,13 +349,26 @@ protected byte[] getFastDigest(PathFragment path) throws IOException {
347349
* @return a new byte array containing the file's digest
348350
* @throws IOException if the digest could not be computed for any reason
349351
*/
350-
protected byte[] getDigest(PathFragment path) throws IOException {
351-
return new ByteSource() {
352-
@Override
353-
public InputStream openStream() throws IOException {
354-
return getInputStream(path);
355-
}
356-
}.hash(digestFunction.getHashFunction()).asBytes();
352+
protected byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
353+
var bs =
354+
new ByteSource() {
355+
@Override
356+
public InputStream openStream() throws IOException {
357+
return getInputStream(path);
358+
}
359+
};
360+
var hasher = digestFunction.getHashFunction().newHasher();
361+
var copied = bs.copyTo(Funnels.asOutputStream(hasher));
362+
if (expectedSize != -1 && copied != expectedSize) {
363+
throw new IOException(
364+
String.format(
365+
Locale.US,
366+
"digesting %s saw %s bytes rather than the expected %s",
367+
path,
368+
copied,
369+
expectedSize));
370+
}
371+
return hasher.hash().asBytes();
357372
}
358373

359374
/**

src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -420,11 +420,11 @@ public void setLastModifiedTime(PathFragment path, long newTime) throws IOExcept
420420
}
421421

422422
@Override
423-
protected byte[] getDigest(PathFragment path) throws IOException {
423+
protected byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
424424
String name = path.toString();
425425
long startTime = Profiler.nanoTimeMaybe();
426426
try {
427-
return super.getDigest(path);
427+
return super.getDigest(path, expectedSize);
428428
} finally {
429429
profiler.logSimpleTask(startTime, ProfilerTask.VFS_MD5, name);
430430
}

src/main/java/com/google/devtools/build/lib/vfs/Path.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,23 @@ public byte[] getFastDigest() throws IOException {
686686
* @throws IOException if the digest could not be computed for any reason
687687
*/
688688
public byte[] getDigest() throws IOException {
689-
return fileSystem.getDigest(asFragment());
689+
return getDigest(-1);
690+
}
691+
692+
/**
693+
* Returns the digest of the file denoted by the current path, following symbolic links. Is not
694+
* guaranteed to call {@link #getFastDigest} internally, even if a fast digest is likely
695+
* available. Callers should prefer {@link DigestUtils#getDigestWithManualFallback} to this method
696+
* unless they know that a fast digest is unavailable and do not need the other features
697+
* (disk-read rate-limiting, global cache) that {@link DigestUtils} provides.
698+
*
699+
* @param expectedSize If not -1, the expected number of bytes to digest. If this does not match
700+
* the number of bytes digested, an {@link IOException} may be raised.
701+
* @return a new byte array containing the file's digest
702+
* @throws IOException if the digest could not be computed for any reason
703+
*/
704+
public byte[] getDigest(long expectedSize) throws IOException {
705+
return fileSystem.getDigest(asFragment(), expectedSize);
690706
}
691707

692708
/**
@@ -716,7 +732,8 @@ public String getDirectoryDigest(XattrProvider xattrProvider) throws IOException
716732
} else {
717733
hasher.putChar('-');
718734
}
719-
hasher.putBytes(DigestUtils.getDigestWithManualFallback(path, xattrProvider));
735+
hasher.putBytes(
736+
DigestUtils.getDigestWithManualFallback(path, stat.getSize(), xattrProvider));
720737
} else if (stat.isDirectory()) {
721738
hasher.putChar('d').putUnencodedChars(path.getDirectoryDigest(xattrProvider));
722739
} else if (stat.isSymbolicLink()) {
@@ -730,7 +747,8 @@ public String getDirectoryDigest(XattrProvider xattrProvider) throws IOException
730747
} else {
731748
hasher.putChar('-');
732749
}
733-
hasher.putBytes(DigestUtils.getDigestWithManualFallback(resolved, xattrProvider));
750+
hasher.putBytes(
751+
DigestUtils.getDigestWithManualFallbackWhenSizeUnknown(resolved, xattrProvider));
734752
} else {
735753
// link to a non-file: include the link itself in the hash
736754
hasher.putChar('l').putUnencodedChars(link.toString());

src/main/java/com/google/devtools/build/lib/vfs/PathTransformingDelegateFileSystem.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ protected byte[] getFastDigest(PathFragment path) throws IOException {
227227
}
228228

229229
@Override
230-
protected byte[] getDigest(PathFragment path) throws IOException {
231-
return delegateFs.getDigest(toDelegatePath(path));
230+
protected byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
231+
return delegateFs.getDigest(toDelegatePath(path), expectedSize);
232232
}
233233

234234
@Override

src/test/java/com/google/devtools/build/lib/analysis/StubbableFSBuildViewTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ protected byte[] getFastDigest(PathFragment path) throws IOException {
137137
if (stubbedFastDigestErrors.containsKey(path)) {
138138
throw stubbedFastDigestErrors.get(path);
139139
}
140-
return getDigest(path);
140+
return getDigest(path, -1);
141141
}
142142
}
143143
}

src/test/java/com/google/devtools/build/lib/exec/SingleBuildFileCacheTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ protected InputStream getInputStream(PathFragment path) throws IOException {
6161
}
6262

6363
@Override
64-
protected byte[] getDigest(PathFragment path) throws IOException {
64+
protected byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
6565
byte[] override = digestOverrides.get(path.getPathString());
66-
return override != null ? override : super.getDigest(path);
66+
return override != null ? override : super.getDigest(path, expectedSize);
6767
}
6868

6969
@Override

src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ protected static final class FakeActionFileSystem extends DelegateFileSystem {
8686

8787
@Override
8888
protected byte[] getFastDigest(PathFragment path) throws IOException {
89-
return super.getDigest(path);
89+
return super.getDigest(path, -1);
9090
}
9191

9292
@Override
93-
protected byte[] getDigest(PathFragment path) throws IOException {
93+
protected byte[] getDigest(PathFragment path, long expectedSize) throws IOException {
9494
throw new UnsupportedOperationException();
9595
}
9696
}

src/test/java/com/google/devtools/build/lib/remote/RemoteActionFileSystemTest.java

+9-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static java.util.Arrays.stream;
2222
import static org.junit.Assert.assertThrows;
2323
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.ArgumentMatchers.anyLong;
2425
import static org.mockito.ArgumentMatchers.eq;
2526
import static org.mockito.Mockito.doAnswer;
2627
import static org.mockito.Mockito.doReturn;
@@ -578,9 +579,9 @@ public void getDigest_fromInputArtifactData_forLocalArtifact() throws Exception
578579
// Verify that we don't fall back to a slow digest.
579580
reset(fs);
580581
assertThat(actionFs.getFastDigest(path)).isEqualTo(getDigest("local contents"));
581-
verify(fs, never()).getDigest(any());
582+
verify(fs, never()).getDigest(any(), anyLong());
582583

583-
assertThat(actionFs.getDigest(path)).isEqualTo(getDigest("local contents"));
584+
assertThat(actionFs.getDigest(path, -1)).isEqualTo(getDigest("local contents"));
584585
}
585586

586587
@Test
@@ -593,9 +594,9 @@ public void getDigest_fromInputArtifactData_forRemoteArtifact() throws Exception
593594
// Verify that we don't fall back to a slow digest.
594595
reset(fs);
595596
assertThat(actionFs.getFastDigest(path)).isEqualTo(getDigest("remote contents"));
596-
verify(fs, never()).getDigest(any());
597+
verify(fs, never()).getDigest(any(), anyLong());
597598

598-
assertThat(actionFs.getDigest(path)).isEqualTo(getDigest("remote contents"));
599+
assertThat(actionFs.getDigest(path, -1)).isEqualTo(getDigest("remote contents"));
599600
}
600601

601602
@Test
@@ -606,7 +607,7 @@ public void getDigest_fromRemoteOutputTree() throws Exception {
606607
injectRemoteFile(actionFs, artifact.getPath().asFragment(), "remote contents");
607608

608609
assertThat(actionFs.getFastDigest(path)).isEqualTo(getDigest("remote contents"));
609-
assertThat(actionFs.getDigest(path)).isEqualTo(getDigest("remote contents"));
610+
assertThat(actionFs.getDigest(path, -1)).isEqualTo(getDigest("remote contents"));
610611
}
611612

612613
@Test
@@ -617,7 +618,7 @@ public void getDigest_fromLocalFilesystem() throws Exception {
617618
writeLocalFile(actionFs, artifact.getPath().asFragment(), "local contents");
618619

619620
assertThat(actionFs.getFastDigest(path)).isNull();
620-
assertThat(actionFs.getDigest(path)).isEqualTo(getDigest("local contents"));
621+
assertThat(actionFs.getDigest(path, -1)).isEqualTo(getDigest("local contents"));
621622
}
622623

623624
@Test
@@ -627,7 +628,7 @@ public void getDigest_notFound() throws Exception {
627628
PathFragment path = artifact.getPath().asFragment();
628629

629630
assertThrows(FileNotFoundException.class, () -> actionFs.getFastDigest(path));
630-
assertThrows(FileNotFoundException.class, () -> actionFs.getDigest(path));
631+
assertThrows(FileNotFoundException.class, () -> actionFs.getDigest(path, -1));
631632
}
632633

633634
@Test
@@ -650,7 +651,7 @@ public void getDigest_followSymlinks(
650651
assertThat(actionFs.getFastDigest(linkPath)).isEqualTo(getDigest("content"));
651652
}
652653

653-
assertThat(actionFs.getDigest(linkPath)).isEqualTo(getDigest("content"));
654+
assertThat(actionFs.getDigest(linkPath, -1)).isEqualTo(getDigest("content"));
654655
}
655656

656657
@Test

0 commit comments

Comments
 (0)