diff --git a/core/lib/Drupal/Component/Utility/ReadOnlyArrayIterator.php b/core/lib/Drupal/Component/Utility/ReadOnlyArrayIterator.php
new file mode 100644
index 0000000..25f16e4
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/ReadOnlyArrayIterator.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Provides an array iterator where indexes are read-only.
+ *
+ * @ingroup utility
+ */
+class ReadOnlyArrayIterator extends \ArrayIterator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetGet($index) {
+    $value = parent::offsetGet($index);
+    if (is_array($value) && !empty($value)) {
+      $value = new ReadOnlyArrayObject($value, \ArrayObject::ARRAY_AS_PROPS);
+    }
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function current() {
+    $value = parent::current();
+    if (is_array($value) && !empty($value)) {
+      $value = new ReadOnlyArrayObject($value, \ArrayObject::ARRAY_AS_PROPS);
+    }
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetSet($index, $newval) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function append($values) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function unserialize($serialized) {}
+
+}
diff --git a/core/lib/Drupal/Component/Utility/ReadOnlyArrayObject.php b/core/lib/Drupal/Component/Utility/ReadOnlyArrayObject.php
new file mode 100644
index 0000000..18e1eef
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/ReadOnlyArrayObject.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Provides an array object where indexes are read-only.
+ *
+ * @ingroup utility
+ */
+class ReadOnlyArrayObject extends \ArrayObject {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetGet($index) {
+    $value = parent::offsetGet($index);
+    if (is_array($value) && !empty($value)) {
+      $value = new ReadOnlyArrayObject($value, \ArrayObject::ARRAY_AS_PROPS);
+    }
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    return new ReadOnlyArrayIterator($this->getArrayCopy(), \ArrayObject::ARRAY_AS_PROPS);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetSet($index, $newval) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function append($values) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function unserialize($serialized) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exchangeArray($serialized) {}
+
+}
diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php
index 21755a5..5f7cdda 100644
--- a/core/lib/Drupal/Core/Template/TwigEnvironment.php
+++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Template;
 
+use Drupal\Component\Utility\ReadOnlyArrayObject;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Render\Markup;
 use Drupal\Core\State\StateInterface;
@@ -140,7 +141,43 @@ public function getTemplateClass($name, $index = NULL) {
   public function renderInline($template_string, array $context = []) {
     // Prefix all inline templates with a special comment.
     $template_string = '{# inline_template_start #}' . $template_string;
+    $this->wrapContext($context);
     return Markup::create($this->loadTemplate($template_string, NULL)->render($context));
   }
 
+  /**
+   * Wraps render arrays with objects, to prevent modification from Twig.
+   *
+   * @param array &$context
+   *   An array of parameters.
+   */
+  public function wrapContext(&$context) {
+    if (is_array($context)) {
+      foreach ($context as $key => $value) {
+        if (is_array($context[$key]) && !empty($context[$key])) {
+          $context[$key] = new ReadOnlyArrayObject($context[$key], \ArrayObject::ARRAY_AS_PROPS);
+        }
+      }
+    }
+  }
+
+  /**
+   * Unwraps read only objects into render arrays.
+   *
+   * @param array &$context
+   *   An array of parameters.
+   */
+  public function unWrapContext(&$context) {
+    if (is_object($context) && $context instanceof ReadOnlyArrayObject) {
+      $context = $context->getArrayCopy();
+    }
+    if (is_array($context)) {
+      foreach ($context as $key => $value) {
+        if (is_object($context[$key]) && $context[$key] instanceof ReadOnlyArrayObject) {
+          $context[$key] = $context[$key]->getArrayCopy();
+        }
+      }
+    }
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index 520a8be..f17b482 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Render\MarkupInterface;
+use Drupal\Component\Utility\ReadOnlyArrayObject;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\Render\AttachmentsInterface;
@@ -300,7 +301,7 @@ public function getLink($text, $url, $attributes = []) {
       '#title' => $text,
       '#url' => $url,
     ];
-    return $build;
+    return new ReadOnlyArrayObject($build, \ArrayObject::ARRAY_AS_PROPS);
   }
 
   /**
@@ -448,7 +449,7 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $
     if (is_scalar($arg)) {
       $return = (string) $arg;
     }
-    elseif (is_object($arg)) {
+    elseif (is_object($arg) && !($arg instanceof ReadOnlyArrayObject)) {
       if ($arg instanceof RenderableInterface) {
         $arg = $arg->toRenderable();
       }
@@ -465,6 +466,9 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $
         throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.');
       }
     }
+    elseif (is_array($arg)) {
+      throw new \Exception('Render arrays cannot be printed.');
+    }
 
     // We have a string or an object converted to a string: Autoescape it!
     if (isset($return)) {
@@ -479,8 +483,9 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $
       return twig_escape_filter($env, $return, $strategy, $charset, $autoescape);
     }
 
-    // This is a normal render array, which is safe by definition, with
+    // This is a wrapped render array, which is safe by definition, with
     // special simple cases already handled.
+    $arg = $arg->getArrayCopy();
 
     // Early return if this element was pre-rendered (no need to re-render).
     if (isset($arg['#printed']) && $arg['#printed'] == TRUE && isset($arg['#markup']) && strlen($arg['#markup']) > 0) {
@@ -561,8 +566,7 @@ public function renderVar($arg) {
     if (is_scalar($arg)) {
       return $arg;
     }
-
-    if (is_object($arg)) {
+    else if (is_object($arg) && !($arg instanceof ReadOnlyArrayObject)) {
       $this->bubbleArgMetadata($arg);
       if ($arg instanceof RenderableInterface) {
         $arg = $arg->toRenderable();
@@ -580,6 +584,11 @@ public function renderVar($arg) {
         throw new \Exception('Object of type ' . get_class($arg) . ' cannot be printed.');
       }
     }
+    elseif (is_array($arg)) {
+      throw new \Exception('Render arrays cannot be printed.');
+    }
+
+    $arg = $arg->getArrayCopy();
 
     // This is a render array, with special simple cases already handled.
     // Early return if this element was pre-rendered (no need to re-render).
diff --git a/core/modules/system/src/Tests/Theme/TwigReadOnlyTest.php b/core/modules/system/src/Tests/Theme/TwigReadOnlyTest.php
new file mode 100644
index 0000000..42db8b9
--- /dev/null
+++ b/core/modules/system/src/Tests/Theme/TwigReadOnlyTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\system\Tests\Theme;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests that variables passed to Twig cannot be modified.
+ *
+ * @group Theme
+ */
+class TwigReadOnlyTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['twig_theme_test'];
+
+  /**
+   * Tests that Twig variables are read-only.
+   */
+  public function testReadOnlyTwigVariables() {
+    $filter_test = [
+      '#theme' => 'twig_theme_test_read_only',
+      '#renderable' => [
+        '#markup' => 'Original',
+      ],
+    ];
+
+    // To modify a ReadOnlyArrayObject, Twig will convert it to an array and
+    // attempt to use that for rendering. This should throw an error.
+    try {
+      $result = \Drupal::service('renderer')->renderRoot($filter_test);
+    }
+    catch (\Exception $e) {}
+    $this->assertFalse(isset($result), 'Variables cannot be modified.');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.read_only.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.read_only.html.twig
new file mode 100644
index 0000000..4bfeec8
--- /dev/null
+++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.read_only.html.twig
@@ -0,0 +1,2 @@
+{% set renderable = renderable|merge({'#markup': 'Modified'}) %}
+{{ renderable }}
diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module
index a8b4086..337dca6 100644
--- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module
+++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module
@@ -73,6 +73,12 @@ function twig_theme_test_theme($existing, $type, $theme, $path) {
     ],
     'template' => 'twig_theme_test.renderable',
   ];
+  $items['twig_theme_test_read_only'] = [
+    'variables' => [
+      'renderable' => NULL,
+    ],
+    'template' => 'twig_theme_test.read_only',
+  ];
   return $items;
 }
 
diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine
index 791f908..760d366 100644
--- a/core/themes/engines/twig/twig.engine
+++ b/core/themes/engines/twig/twig.engine
@@ -52,7 +52,7 @@ function twig_init(Extension $theme) {
  *   The output generated by the template, plus any debug information.
  */
 function twig_render_template($template_file, array $variables) {
-  /** @var \Twig_Environment $twig_service */
+  /** @var \Drupal\Core\Template\TwigEnvironment $twig_service */
   $twig_service = \Drupal::service('twig');
   $output = [
     'debug_prefix' => '',
@@ -61,7 +61,9 @@ function twig_render_template($template_file, array $variables) {
     'debug_suffix' => '',
   ];
   try {
+    $twig_service->wrapContext($variables);
     $output['rendered_markup'] = $twig_service->loadTemplate($template_file)->render($variables);
+    $twig_service->unWrapContext($variables);
   }
   catch (\Twig_Error_Runtime $e) {
     // In case there is a previous exception, re-throw the previous exception,
