Skip to content

Commit 5a1b258

Browse files
committed
Force rctx.{download_and,}extract to create user-readable files
Archives in the wild do sometimes contain non-readable files, but other tools work around this and thus mask their brokenness.
1 parent 0bc73b2 commit 5a1b258

4 files changed

Lines changed: 126 additions & 3 deletions

File tree

src/main/java/com/google/devtools/build/lib/bazel/repository/decompressor/ArFunction.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ public Path decompress(DecompressorDescriptor descriptor)
7575
try (OutputStream out = filePath.getOutputStream()) {
7676
ByteStreams.copy(arStream, out);
7777
}
78-
filePath.chmod(entry.getMode());
78+
// Ensure that all files are at least user-readable. Some archives contain files that
79+
// are not, but many other tools are working around this and thus mask these issues.
80+
filePath.chmod(entry.getMode() | 0400);
7981
// entry.getLastModified() appears to be in seconds, so we need to convert
8082
// it into milliseconds for setLastModifiedTime
8183
filePath.setLastModifiedTime(entry.getLastModified() * 1000L);

src/main/java/com/google/devtools/build/lib/bazel/repository/decompressor/CompressedTarFunction.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ public Path decompress(DecompressorDescriptor descriptor)
164164
try (OutputStream out = filePath.getOutputStream()) {
165165
ByteStreams.copy(tarStream, out);
166166
}
167-
filePath.chmod(entry.getMode());
167+
// Ensure that all files are at least user-readable. Some archives contain files that
168+
// are not, but many other tools are working around this and thus mask these issues.
169+
filePath.chmod(entry.getMode() | 0400);
168170

169171
// This can only be done on real files, not links, or it will skip the reader to
170172
// the next "real" file to try to find the mod time info.

src/main/java/com/google/devtools/build/lib/bazel/repository/decompressor/ZipDecompressor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ private static void extractZipEntry(
172172
throw new InterruptedException();
173173
}
174174
}
175-
outputPath.chmod(permissions);
175+
// Ensure that all files are at least user-readable. Some archives contain files that
176+
// are not, but many other tools are working around this and thus mask these issues.
177+
outputPath.chmod(permissions | 0400);
176178
outputPath.setLastModifiedTime(entry.getTime());
177179
}
178180
}

src/test/shell/bazel/starlark_repository_test.sh

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3598,6 +3598,123 @@ EOF
35983598
[[ -f "$output_base/external/+repo+foo/ruff" ]] || fail "Expected ruff binary to be extracted"
35993599
}
36003600

3601+
# Verifies that files without user-readable permissions in archives are made
3602+
# readable after extraction.
3603+
function test_extract_non_readable_file_tar() {
3604+
local archive_tar="${TEST_TMPDIR}/non_readable.tar.gz"
3605+
3606+
python3 -c "
3607+
import tarfile
3608+
import io
3609+
import gzip
3610+
3611+
content = b'secret content'
3612+
with gzip.open('${archive_tar}', 'wb') as gz:
3613+
with tarfile.open(fileobj=gz, mode='w') as tar:
3614+
info = tarfile.TarInfo(name='non_readable_dir/non_readable.txt')
3615+
info.size = len(content)
3616+
info.mode = 0o000 # No permissions
3617+
tar.addfile(info, io.BytesIO(content))
3618+
"
3619+
3620+
cat > $(setup_module_dot_bazel) <<EOF
3621+
repo = use_repo_rule('//:test.bzl', 'repo')
3622+
repo(name = 'foo')
3623+
EOF
3624+
touch BUILD
3625+
3626+
cat >test.bzl <<EOF
3627+
def _impl(repository_ctx):
3628+
repository_ctx.extract('${archive_tar}', 'out_dir')
3629+
# Verify the file is readable by reading it
3630+
content = repository_ctx.read('out_dir/non_readable_dir/non_readable.txt')
3631+
if 'secret content' not in content:
3632+
fail('Expected to read file content, got: ' + content)
3633+
repository_ctx.file("BUILD", "filegroup(name='bar', srcs=[])")
3634+
3635+
repo = repository_rule(implementation=_impl)
3636+
EOF
3637+
3638+
bazel build @foo//:bar >& $TEST_log || fail "Failed to build"
3639+
}
3640+
3641+
function test_extract_non_readable_file_zip() {
3642+
local archive_zip="${TEST_TMPDIR}/non_readable.zip"
3643+
3644+
pushd "${TEST_TMPDIR}"
3645+
mkdir -p non_readable_zip_dir
3646+
echo "secret zip content" > non_readable_zip_dir/non_readable.txt
3647+
python3 -c "
3648+
import zipfile
3649+
with zipfile.ZipFile('non_readable.zip', 'w') as zf:
3650+
info = zipfile.ZipInfo('non_readable_zip_dir/non_readable.txt')
3651+
# S_IFREG (0o100000) marks it as a regular file, but with no permission bits.
3652+
# This ensures getPermissions() returns the actual mode rather than defaulting to 0755.
3653+
info.external_attr = 0o100000 << 16
3654+
zf.writestr(info, 'secret zip content')
3655+
"
3656+
popd
3657+
3658+
cat > $(setup_module_dot_bazel) <<EOF
3659+
repo = use_repo_rule('//:test.bzl', 'repo')
3660+
repo(name = 'foo')
3661+
EOF
3662+
touch BUILD
3663+
3664+
cat >test.bzl <<EOF
3665+
def _impl(repository_ctx):
3666+
repository_ctx.extract('${archive_zip}', 'out_dir')
3667+
# Verify the file is readable by reading it
3668+
content = repository_ctx.read('out_dir/non_readable_zip_dir/non_readable.txt')
3669+
if 'secret zip content' not in content:
3670+
fail('Expected to read file content, got: ' + content)
3671+
repository_ctx.file("BUILD", "filegroup(name='bar', srcs=[])")
3672+
3673+
repo = repository_rule(implementation=_impl)
3674+
EOF
3675+
3676+
bazel build @foo//:bar >& $TEST_log || fail "Failed to build"
3677+
}
3678+
3679+
function test_extract_non_readable_file_ar() {
3680+
local archive_ar="${TEST_TMPDIR}/non_readable.ar"
3681+
3682+
# Create a valid AR file, then patch the permission field to 0000
3683+
pushd "${TEST_TMPDIR}"
3684+
echo "secret ar content" > non_readable.txt
3685+
ar rc non_readable.ar non_readable.txt
3686+
# Patch the mode field (bytes 40-47 after the 8-byte magic) to "0 "
3687+
python3 -c "
3688+
with open('non_readable.ar', 'r+b') as f:
3689+
# AR magic is 8 bytes, then header starts
3690+
# Header format: filename(16) + mtime(12) + owner(6) + group(6) + mode(8) + size(10) + magic(2)
3691+
# Mode is at offset 8 + 16 + 12 + 6 + 6 = 48
3692+
f.seek(48)
3693+
f.write(b'0 ') # 8 bytes for mode 0000 in octal
3694+
"
3695+
popd
3696+
3697+
cat > $(setup_module_dot_bazel) <<EOF
3698+
repo = use_repo_rule('//:test.bzl', 'repo')
3699+
repo(name = 'foo')
3700+
EOF
3701+
touch BUILD
3702+
3703+
cat >test.bzl <<EOF
3704+
def _impl(repository_ctx):
3705+
repository_ctx.extract('${archive_ar}', 'out_dir')
3706+
# Verify the file is readable by reading it
3707+
content = repository_ctx.read('out_dir/non_readable.txt')
3708+
if 'secret ar content' not in content:
3709+
fail('Expected to read file content, got: ' + content)
3710+
repository_ctx.file("BUILD", "filegroup(name='bar', srcs=[])")
3711+
3712+
repo = repository_rule(implementation=_impl)
3713+
EOF
3714+
3715+
bazel build @foo//:bar >& $TEST_log || fail "Failed to build"
3716+
}
3717+
36013718
# Regression test for https://github.com/bazelbuild/bazel/issues/27446.
36023719
function do_test_local_module_file_patch() {
36033720
cat > $(setup_module_dot_bazel) <<'EOF'

0 commit comments

Comments
 (0)