From 846f61f9817e65d891c2d6e9d09d179815b57e15 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 18:28:13 +0200 Subject: [PATCH 1/9] Mount docs source read-only in Docker build container --- build_docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_docs b/build_docs index 94c8860b3a849..4341d3e90e053 100755 --- a/build_docs +++ b/build_docs @@ -555,7 +555,7 @@ def standard_docker_args(): raise ArgError("This process isn't likely to suceed if run as root") docker_args.extend(['--user', '%d:%d' % (uid, getgid())]) # Mount the docs build code so we can run it! - docker_args.extend(['-v', '%s:/docs_build:cached' % DIR]) + docker_args.extend(['-v', '%s:/docs_build:ro,cached' % DIR]) # Seccomp adds a *devestating* performance overhead if you happen # to have it installed. docker_args.extend(['--security-opt', 'seccomp=unconfined']) From 3b09312a8c06c20b359d5210e03f007d1fd32a20 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 18:38:55 +0200 Subject: [PATCH 2/9] Redirect rubocop cache to /tmp for read-only mount compatibility --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 86a27b026501d..bcfa3d2459f7f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,7 +5,7 @@ require: AllCops: TargetRubyVersion: 2.3 - CacheRootDirectory: .rubocop_cache + CacheRootDirectory: /tmp/rubocop_cache MaxFilesInCache: 1000 Style/ReturnNil: From 1f540980a71e931930517553415267160e081172 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 18:52:42 +0200 Subject: [PATCH 3/9] Add writable overlay for resources/web/tests to allow parcel output --- build_docs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build_docs b/build_docs index 4341d3e90e053..095a1d01b1cf5 100755 --- a/build_docs +++ b/build_docs @@ -556,6 +556,10 @@ def standard_docker_args(): docker_args.extend(['--user', '%d:%d' % (uid, getgid())]) # Mount the docs build code so we can run it! docker_args.extend(['-v', '%s:/docs_build:ro,cached' % DIR]) + # The web test suite uses parcel to build bundles into resources/web/tests/ + # before jest reads them. That generated output dir needs to be writable; + # this bind-mount shadows the ro parent for that path only. + docker_args.extend(['-v', '%s/resources/web/tests:/docs_build/resources/web/tests' % DIR]) # Seccomp adds a *devestating* performance overhead if you happen # to have it installed. docker_args.extend(['--security-opt', 'seccomp=unconfined']) From 80ed4a273d6529e10613c52a911707a11a61ae33 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 18:55:28 +0200 Subject: [PATCH 4/9] Move web/tests writable overlay to --docker-run only, not doc builds --- build_docs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build_docs b/build_docs index 095a1d01b1cf5..c7bb3357d4b75 100755 --- a/build_docs +++ b/build_docs @@ -556,10 +556,6 @@ def standard_docker_args(): docker_args.extend(['--user', '%d:%d' % (uid, getgid())]) # Mount the docs build code so we can run it! docker_args.extend(['-v', '%s:/docs_build:ro,cached' % DIR]) - # The web test suite uses parcel to build bundles into resources/web/tests/ - # before jest reads them. That generated output dir needs to be writable; - # this bind-mount shadows the ro parent for that path only. - docker_args.extend(['-v', '%s/resources/web/tests:/docs_build/resources/web/tests' % DIR]) # Seccomp adds a *devestating* performance overhead if you happen # to have it installed. docker_args.extend(['--security-opt', 'seccomp=unconfined']) @@ -603,6 +599,9 @@ if __name__ == '__main__': build_docker_image(image) cmd = ['docker', 'run'] cmd.extend(standard_docker_args()) + # The web test suite writes parcel output here before jest reads it; + # shadow the ro parent mount with a writable bind for this path only. + cmd.extend(['-v', '%s/resources/web/tests:/docs_build/resources/web/tests' % DIR]) cmd.extend(['--workdir', docker_cwd]) if stdout.isatty(): cmd.append('-t') From 55662603d141bcfc70b98283c3a5ef1c6d70329b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 19:19:00 +0200 Subject: [PATCH 5/9] Fix pycodestyle E501 line-too-long on writable overlay mount --- build_docs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build_docs b/build_docs index c7bb3357d4b75..56bd47221a87b 100755 --- a/build_docs +++ b/build_docs @@ -599,9 +599,12 @@ if __name__ == '__main__': build_docker_image(image) cmd = ['docker', 'run'] cmd.extend(standard_docker_args()) - # The web test suite writes parcel output here before jest reads it; - # shadow the ro parent mount with a writable bind for this path only. - cmd.extend(['-v', '%s/resources/web/tests:/docs_build/resources/web/tests' % DIR]) + # The web test suite writes parcel output into resources/web/tests/ + # before jest reads it; shadow the ro parent for this path only. + cmd.extend(['-v', ( + '%s/resources/web/tests' + ':/docs_build/resources/web/tests' % DIR + )]) cmd.extend(['--workdir', docker_cwd]) if stdout.isatty(): cmd.append('-t') From 02c0370d839e6034bc34a42c0bd5c3fab3f24971 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 19:30:55 +0200 Subject: [PATCH 6/9] Ensure web/tests dir exists before bind-mounting to avoid root ownership When the Makefile runs `rm -rf tests` before invoking build_docs --docker-run, the directory no longer exists on the host. Docker auto-creates missing bind-mount targets as root, causing EACCES for the non-root container user. Pre-create the directory with makedirs so the mount is owned by the current user. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- build_docs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/build_docs b/build_docs index 56bd47221a87b..e1acf18236226 100755 --- a/build_docs +++ b/build_docs @@ -17,7 +17,7 @@ import errno import logging -from os import environ, getgid, getuid +from os import environ, getgid, getuid, makedirs from os.path import basename, dirname, exists, expanduser, isdir from os.path import join, normpath, realpath import re @@ -601,9 +601,13 @@ if __name__ == '__main__': cmd.extend(standard_docker_args()) # The web test suite writes parcel output into resources/web/tests/ # before jest reads it; shadow the ro parent for this path only. + # Ensure the directory exists before mounting so Docker doesn't + # auto-create it as root, which would deny writes to the container + # user. + web_tests_dir = '%s/resources/web/tests' % DIR + makedirs(web_tests_dir, exist_ok=True) cmd.extend(['-v', ( - '%s/resources/web/tests' - ':/docs_build/resources/web/tests' % DIR + '%s:/docs_build/resources/web/tests' % web_tests_dir )]) cmd.extend(['--workdir', docker_cwd]) if stdout.isatty(): From 3a718ad90933bd696cdf590bcb5f925433e45700 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 19:53:37 +0200 Subject: [PATCH 7/9] Mount tmpfs at /docs_build/.repos for integration test air-gapped setup The integration tests write to /docs_build/.repos/target_repo.git in start_air_gapped. Shadow that path with a writable tmpfs so the test can create its own repo there without violating the ro source mount. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- build_docs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_docs b/build_docs index e1acf18236226..921c0179e4626 100755 --- a/build_docs +++ b/build_docs @@ -609,6 +609,9 @@ if __name__ == '__main__': cmd.extend(['-v', ( '%s:/docs_build/resources/web/tests' % web_tests_dir )]) + # The integration tests write to /docs_build/.repos/ (air-gapped + # preview setup). Shadow it with a tmpfs so the source stays ro. + cmd.extend(['--tmpfs', '/docs_build/.repos']) cmd.extend(['--workdir', docker_cwd]) if stdout.isatty(): cmd.append('-t') From 5ce7cf1dcef3974eef768d63dee2df1ed7d716a5 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 20:15:55 +0200 Subject: [PATCH 8/9] Add tmpfs for parcel caches and fix rspec_status path --- build_docs | 3 +++ integtest/spec/spec_helper.rb | 2 +- resources/asciidoctor/spec/spec_helper.rb | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build_docs b/build_docs index 921c0179e4626..b5245fd308223 100755 --- a/build_docs +++ b/build_docs @@ -573,6 +573,9 @@ def standard_docker_args(): '--tmpfs', '/var/lib/nginx/proxy', '--tmpfs', '/var/lib/nginx/uwsgi', '--tmpfs', '/var/lib/nginx/scgi']) + # Parcel writes build caches here; use tmpfs so the ro source stays clean. + docker_args.extend(['--tmpfs', '/docs_build/.cache', + '--tmpfs', '/docs_build/resources/web/.cache']) # Mount in a custom gitconfig that treats all directories as safe docker_args.extend(['-v', '%s/gitconfig:/.gitconfig' % DIR]) return docker_args diff --git a/integtest/spec/spec_helper.rb b/integtest/spec/spec_helper.rb index 2b1f1cdcfe96f..2d70973eb2379 100644 --- a/integtest/spec/spec_helper.rb +++ b/integtest/spec/spec_helper.rb @@ -23,7 +23,7 @@ RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' + config.example_status_persistence_file_path = '/tmp/.rspec_status' # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! diff --git a/resources/asciidoctor/spec/spec_helper.rb b/resources/asciidoctor/spec/spec_helper.rb index 69acff5383467..7a964ed426ee2 100644 --- a/resources/asciidoctor/spec/spec_helper.rb +++ b/resources/asciidoctor/spec/spec_helper.rb @@ -2,7 +2,7 @@ RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '.rspec_status' + config.example_status_persistence_file_path = '/tmp/.rspec_status' # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! From 7b7cb2ad6a1e3e979ea7c88accb145df20677bc3 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Sun, 3 May 2026 20:33:45 +0200 Subject: [PATCH 9/9] Protect .buildkite/hooks with tmpfs instead of making all of /docs_build ro Mounting /docs_build:ro broke the integration tests (writes to .repos, .cache etc.) and Docker prevents tmpfs mounts inside a ro bind mount. Instead keep /docs_build writable and shadow only .buildkite/hooks with an empty tmpfs. Any code running inside the container can write there but the writes go to an ephemeral tmpfs, never reaching the host -- so the Buildkite hook-injection attack vector is closed without breaking builds. --- build_docs | 21 ++++----------------- integtest/spec/spec_helper.rb | 2 +- resources/asciidoctor/spec/spec_helper.rb | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/build_docs b/build_docs index b5245fd308223..505f7dac9dc1c 100755 --- a/build_docs +++ b/build_docs @@ -555,7 +555,10 @@ def standard_docker_args(): raise ArgError("This process isn't likely to suceed if run as root") docker_args.extend(['--user', '%d:%d' % (uid, getgid())]) # Mount the docs build code so we can run it! - docker_args.extend(['-v', '%s:/docs_build:ro,cached' % DIR]) + docker_args.extend(['-v', '%s:/docs_build:cached' % DIR]) + # Shadow .buildkite/hooks with an empty tmpfs so any code running inside + # the container cannot plant Buildkite hooks that execute on the host. + docker_args.extend(['--tmpfs', '/docs_build/.buildkite/hooks']) # Seccomp adds a *devestating* performance overhead if you happen # to have it installed. docker_args.extend(['--security-opt', 'seccomp=unconfined']) @@ -573,9 +576,6 @@ def standard_docker_args(): '--tmpfs', '/var/lib/nginx/proxy', '--tmpfs', '/var/lib/nginx/uwsgi', '--tmpfs', '/var/lib/nginx/scgi']) - # Parcel writes build caches here; use tmpfs so the ro source stays clean. - docker_args.extend(['--tmpfs', '/docs_build/.cache', - '--tmpfs', '/docs_build/resources/web/.cache']) # Mount in a custom gitconfig that treats all directories as safe docker_args.extend(['-v', '%s/gitconfig:/.gitconfig' % DIR]) return docker_args @@ -602,19 +602,6 @@ if __name__ == '__main__': build_docker_image(image) cmd = ['docker', 'run'] cmd.extend(standard_docker_args()) - # The web test suite writes parcel output into resources/web/tests/ - # before jest reads it; shadow the ro parent for this path only. - # Ensure the directory exists before mounting so Docker doesn't - # auto-create it as root, which would deny writes to the container - # user. - web_tests_dir = '%s/resources/web/tests' % DIR - makedirs(web_tests_dir, exist_ok=True) - cmd.extend(['-v', ( - '%s:/docs_build/resources/web/tests' % web_tests_dir - )]) - # The integration tests write to /docs_build/.repos/ (air-gapped - # preview setup). Shadow it with a tmpfs so the source stays ro. - cmd.extend(['--tmpfs', '/docs_build/.repos']) cmd.extend(['--workdir', docker_cwd]) if stdout.isatty(): cmd.append('-t') diff --git a/integtest/spec/spec_helper.rb b/integtest/spec/spec_helper.rb index 2d70973eb2379..2b1f1cdcfe96f 100644 --- a/integtest/spec/spec_helper.rb +++ b/integtest/spec/spec_helper.rb @@ -23,7 +23,7 @@ RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '/tmp/.rspec_status' + config.example_status_persistence_file_path = '.rspec_status' # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! diff --git a/resources/asciidoctor/spec/spec_helper.rb b/resources/asciidoctor/spec/spec_helper.rb index 7a964ed426ee2..69acff5383467 100644 --- a/resources/asciidoctor/spec/spec_helper.rb +++ b/resources/asciidoctor/spec/spec_helper.rb @@ -2,7 +2,7 @@ RSpec.configure do |config| # Enable flags like --only-failures and --next-failure - config.example_status_persistence_file_path = '/tmp/.rspec_status' + config.example_status_persistence_file_path = '.rspec_status' # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching!