Repository: readthedocs/readthedocs.org

Issue:
Build: add gvisor project feature flag for testing runsc runtime
We'll have some production operations changes to make to support this, but for now I'd like to start off as a feature flag that will only work in local development for now. I could also make this an application setting I suppose, but we probably want the feature flag for private beta testing this in production anyways.

This adds additional protection to arbitrary command execution that will be enabled with build.jobs and build.commands build configuration options.


Discussion:

Resolving patch:
diff --git a/readthedocs/doc_builder/environments.py b/readthedocs/doc_builder/environments.py
--- a/readthedocs/doc_builder/environments.py
+++ b/readthedocs/doc_builder/environments.py
@@ -1,6 +1,5 @@
 """Documentation Builder Environments."""
 
-import structlog
 import os
 import re
 import subprocess
@@ -8,6 +7,7 @@
 import uuid
 from datetime import datetime
 
+import structlog
 from django.conf import settings
 from django.utils.translation import gettext_lazy as _
 from docker import APIClient
@@ -31,10 +31,7 @@
     DOCKER_TIMEOUT_EXIT_CODE,
     DOCKER_VERSION,
 )
-from .exceptions import (
-    BuildAppError,
-    BuildUserError,
-)
+from .exceptions import BuildAppError, BuildUserError
 
 log = structlog.get_logger(__name__)
 
@@ -790,10 +787,16 @@ def create_container(self):
         """Create docker container."""
         client = self.get_client()
         try:
+            docker_runtime = self.project.get_feature_value(
+                Feature.DOCKER_GVISOR_RUNTIME,
+                positive="runsc",
+                negative=None,
+            )
             log.info(
                 'Creating Docker container.',
                 container_image=self.container_image,
                 container_id=self.container_id,
+                docker_runtime=docker_runtime,
             )
             self.container = client.create_container(
                 image=self.container_image,
@@ -808,6 +811,7 @@ def create_container(self):
                 host_config=self.get_container_host_config(),
                 detach=True,
                 user=settings.RTD_DOCKER_USER,
+                runtime=docker_runtime,
             )
             client.start(container=self.container_id)
         except (DockerAPIError, ConnectionError) as e:
diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py
--- a/readthedocs/projects/models.py
+++ b/readthedocs/projects/models.py
@@ -2,12 +2,12 @@
 import fnmatch
 import hashlib
 import hmac
-import structlog
 import os
 import re
 from shlex import quote
 from urllib.parse import urlparse
 
+import structlog
 from allauth.socialaccount.providers import registry as allauth_registry
 from django.conf import settings
 from django.conf.urls import include
@@ -21,10 +21,7 @@
 from django.utils.functional import cached_property
 from django.utils.translation import gettext_lazy as _
 from django.views import defaults
-from django_extensions.db.fields import (
-    CreationDateTimeField,
-    ModificationDateTimeField,
-)
+from django_extensions.db.fields import CreationDateTimeField, ModificationDateTimeField
 from django_extensions.db.models import TimeStampedModel
 from taggit.managers import TaggableManager
 
@@ -34,7 +31,6 @@
 from readthedocs.core.history import ExtraHistoricalRecords
 from readthedocs.core.resolver import resolve, resolve_domain
 from readthedocs.core.utils import slugify
-from readthedocs.doc_builder.constants import DOCKER_LIMITS
 from readthedocs.projects import constants
 from readthedocs.projects.exceptions import ProjectConfigurationError
 from readthedocs.projects.managers import HTMLFileManager
@@ -54,12 +50,7 @@
 from readthedocs.storage import build_media_storage
 from readthedocs.vcs_support.backends import backend_cls
 
-from .constants import (
-    MEDIA_TYPE_EPUB,
-    MEDIA_TYPE_HTMLZIP,
-    MEDIA_TYPE_PDF,
-    MEDIA_TYPES,
-)
+from .constants import MEDIA_TYPE_EPUB, MEDIA_TYPE_HTMLZIP, MEDIA_TYPE_PDF, MEDIA_TYPES
 
 log = structlog.get_logger(__name__)
 
@@ -676,9 +667,9 @@ def proxito_urlconf(self):
         It is used for doc serving on projects that have their own ``urlconf``.
         """
         from readthedocs.projects.views.public import ProjectDownloadMedia
+        from readthedocs.proxito.urls import core_urls
         from readthedocs.proxito.views.serve import ServeDocs
         from readthedocs.proxito.views.utils import proxito_404_page_handler
-        from readthedocs.proxito.urls import core_urls
 
         class ProxitoURLConf:
 
@@ -1753,20 +1744,21 @@ def add_features(sender, **kwargs):
 
     # Feature constants - this is not a exhaustive list of features, features
     # may be added by other packages
-    ALLOW_DEPRECATED_WEBHOOKS = 'allow_deprecated_webhooks'
-    DONT_OVERWRITE_SPHINX_CONTEXT = 'dont_overwrite_sphinx_context'
-    MKDOCS_THEME_RTD = 'mkdocs_theme_rtd'
-    API_LARGE_DATA = 'api_large_data'
-    DONT_SHALLOW_CLONE = 'dont_shallow_clone'
-    USE_TESTING_BUILD_IMAGE = 'use_testing_build_image'
-    CLEAN_AFTER_BUILD = 'clean_after_build'
-    UPDATE_CONDA_STARTUP = 'update_conda_startup'
-    CONDA_APPEND_CORE_REQUIREMENTS = 'conda_append_core_requirements'
-    CONDA_USES_MAMBA = 'conda_uses_mamba'
-    ALL_VERSIONS_IN_HTML_CONTEXT = 'all_versions_in_html_context'
-    CACHED_ENVIRONMENT = 'cached_environment'
-    LIMIT_CONCURRENT_BUILDS = 'limit_concurrent_builds'
-    CDN_ENABLED = 'cdn_enabled'
+    ALLOW_DEPRECATED_WEBHOOKS = "allow_deprecated_webhooks"
+    DONT_OVERWRITE_SPHINX_CONTEXT = "dont_overwrite_sphinx_context"
+    MKDOCS_THEME_RTD = "mkdocs_theme_rtd"
+    API_LARGE_DATA = "api_large_data"
+    DONT_SHALLOW_CLONE = "dont_shallow_clone"
+    USE_TESTING_BUILD_IMAGE = "use_testing_build_image"
+    CLEAN_AFTER_BUILD = "clean_after_build"
+    UPDATE_CONDA_STARTUP = "update_conda_startup"
+    CONDA_APPEND_CORE_REQUIREMENTS = "conda_append_core_requirements"
+    CONDA_USES_MAMBA = "conda_uses_mamba"
+    ALL_VERSIONS_IN_HTML_CONTEXT = "all_versions_in_html_context"
+    CACHED_ENVIRONMENT = "cached_environment"
+    LIMIT_CONCURRENT_BUILDS = "limit_concurrent_builds"
+    CDN_ENABLED = "cdn_enabled"
+    DOCKER_GVISOR_RUNTIME = "gvisor_runtime"
 
     # Versions sync related features
     SKIP_SYNC_TAGS = 'skip_sync_tags'
@@ -1854,6 +1846,10 @@ def add_features(sender, **kwargs):
             CDN_ENABLED,
             _('CDN support for a project\'s public versions when privacy levels are enabled.'),
         ),
+        (
+            DOCKER_GVISOR_RUNTIME,
+            _("Use Docker gVisor runtime to create build container."),
+        ),
 
         # Versions sync related features
         (


Test patch: