Snap for 12735943 from a6e40b47d8cd01b0cb48752b9fc5822e2f79de0d to 25Q1-release

Change-Id: Ia36874aab9a16149dfa0f5322b54dfe0314af0ab
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 495598b..7dab58d 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -46,7 +46,8 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <activity android:name=".SettingsActivity" />
+        <activity android:name=".SettingsActivity"
+            android:label="@string/action_settings" />
         <activity android:name=".SettingsDiskResizeActivity"
             android:label="@string/settings_disk_resize_title" />
         <activity android:name=".SettingsPortForwardingActivity"
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
index b2a2085..7f14179 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
@@ -16,8 +16,11 @@
 
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.os.Build;
 import android.os.Environment;
+import android.util.Log;
 
 import org.apache.commons.compress.archivers.ArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
@@ -43,7 +46,8 @@
 class ImageArchive {
     private static final String DIR_IN_SDCARD = "linux";
     private static final String ARCHIVE_NAME = "images.tar.gz";
-    private static final String HOST_URL = "https://dl.google.com/android/ferrochrome/latest";
+    private static final String BUILD_TAG = "latest"; // TODO: use actual tag name
+    private static final String HOST_URL = "https://dl.google.com/android/ferrochrome/" + BUILD_TAG;
 
     // Only one can be non-null
     private final URL mUrl;
@@ -138,6 +142,8 @@
      * an additional input stream which will be used during the installation.
      */
     public void installTo(Path dir, Function<InputStream, InputStream> filter) throws IOException {
+        String source = mPath != null ? mPath.toString() : mUrl.toString();
+        Log.d(TAG, "Installing. source: " + source + ", destination: " + dir.toString());
         try (InputStream stream = getInputStream(filter);
                 GzipCompressorInputStream gzStream = new GzipCompressorInputStream(stream);
                 TarArchiveInputStream tarStream = new TarArchiveInputStream(gzStream)) {
@@ -148,9 +154,9 @@
                 Path to = dir.resolve(entry.getName());
                 if (Files.isDirectory(to)) {
                     Files.createDirectories(to);
-                } else {
-                    Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING);
+                    continue;
                 }
+                Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING);
             }
         }
         commitInstallationAt(dir);
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
index f318358..318f49a 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.java
@@ -23,7 +23,9 @@
 import android.system.Os;
 import android.util.Log;
 
+import java.io.BufferedReader;
 import java.io.FileDescriptor;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.file.Files;
@@ -36,6 +38,7 @@
     private static final String ROOTFS_FILENAME = "root_part";
     private static final String BACKUP_FILENAME = "root_part_backup";
     private static final String CONFIG_FILENAME = "vm_config.json";
+    private static final String BUILD_ID_FILENAME = "build_id";
     static final String MARKER_FILENAME = "completed";
 
     public static final long RESIZE_STEP_BYTES = 4 << 20; // 4 MiB
@@ -45,6 +48,7 @@
     private final Path mBackup;
     private final Path mConfig;
     private final Path mMarker;
+    private String mBuildId;
 
     /** Returns InstalledImage for a given app context */
     public static InstalledImage getDefault(Context context) {
@@ -79,6 +83,26 @@
         return mConfig;
     }
 
+    /** Returns the build ID of the installed image */
+    public String getBuildId() {
+        if (mBuildId == null) {
+            mBuildId = readBuildId();
+        }
+        return mBuildId;
+    }
+
+    private String readBuildId() {
+        Path file = mDir.resolve(BUILD_ID_FILENAME);
+        if (!Files.exists(file)) {
+            return "<no build id>";
+        }
+        try (BufferedReader r = new BufferedReader(new FileReader(file.toFile()))) {
+            return r.readLine();
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to read build ID", e);
+        }
+    }
+
     public Path uninstallAndBackup() throws IOException {
         Files.delete(mMarker);
         Files.move(mRootPartition, mBackup, StandardCopyOption.REPLACE_EXISTING);
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index c2b3fd4..ac05d78 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -48,11 +48,6 @@
 public class InstallerService extends Service {
     private static final int NOTIFICATION_ID = 1313; // any unique number among notifications
 
-    private static final String IMAGE_URL =
-            Arrays.asList(Build.SUPPORTED_ABIS).contains("x86_64")
-                    ? "https://dl.google.com/android/ferrochrome/latest/x86_64/images.tar.gz"
-                    : "https://dl.google.com/android/ferrochrome/latest/aarch64/images.tar.gz";
-
     private final Object mLock = new Object();
 
     private Notification mNotification;
@@ -191,8 +186,6 @@
 
     // TODO(b/374015561): Support pause/resume download
     private boolean downloadFromUrl(boolean isWifiOnly) {
-        Log.i(TAG, "trying to download from " + IMAGE_URL);
-
         if (!checkForWifiOnly(isWifiOnly)) {
             Log.e(TAG, "Install isn't started because Wifi isn't available");
             notifyError(getString(R.string.installer_error_no_wifi));
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 0f62984..eab737b 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
@@ -52,6 +53,7 @@
 import androidx.activity.result.ActivityResult;
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -101,6 +103,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        lockOrientationIfNecessary();
 
         mImage = InstalledImage.getDefault(this);
 
@@ -135,7 +138,7 @@
                 .getRootView()
                 .setOnApplyWindowInsetsListener(
                         (v, insets) -> {
-                            updateKeyboardContainerVisibility();
+                            updateModifierKeysVisibility();
                             return insets;
                         });
         // if installer is launched, it will be handled in onActivityResult
@@ -148,6 +151,23 @@
         }
     }
 
+    private void lockOrientationIfNecessary() {
+        boolean hasHwQwertyKeyboard =
+                getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;
+        if (hasHwQwertyKeyboard) {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+        } else if (getResources().getBoolean(R.bool.terminal_portrait_only)) {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        lockOrientationIfNecessary();
+        updateModifierKeysVisibility();
+    }
+
     private void setupModifierKeys() {
         // Only ctrl key is special, it communicates with xtermjs to modify key event with ctrl key
         findViewById(R.id.btn_ctrl)
@@ -279,12 +299,7 @@
                                             findViewById(R.id.webview_container)
                                                     .setVisibility(View.VISIBLE);
                                             mBootCompleted.open();
-                                            // TODO(b/376813452): support talkback as well
-                                            int keyVisibility =
-                                                    mAccessibilityManager.isEnabled()
-                                                            ? View.GONE
-                                                            : View.VISIBLE;
-                                            updateKeyboardContainerVisibility();
+                                            updateModifierKeysVisibility();
                                         }
                                     }
                                 });
@@ -384,14 +399,15 @@
         connectToTerminalService();
     }
 
-    private void updateKeyboardContainerVisibility() {
-        boolean imeVisible =
-                this.getWindow()
-                        .getDecorView()
-                        .getRootWindowInsets()
-                        .isVisible(WindowInsets.Type.ime());
-        View keyboardContainer = findViewById(R.id.keyboard_container);
-        keyboardContainer.setVisibility(!imeVisible ? View.GONE : View.VISIBLE);
+    private void updateModifierKeysVisibility() {
+        boolean imeShown =
+                getWindow().getDecorView().getRootWindowInsets().isVisible(WindowInsets.Type.ime());
+        boolean hasHwQwertyKeyboard =
+                getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;
+        boolean showModifierKeys = imeShown && !hasHwQwertyKeyboard;
+
+        View modifierKeys = findViewById(R.id.modifier_keys);
+        modifierKeys.setVisibility(showModifierKeys ? View.VISIBLE : View.GONE);
     }
 
     @Override
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
index 784e740..a4a0a84 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
@@ -16,9 +16,6 @@
 package com.android.virtualization.terminal
 
 import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.view.WindowManager
 import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -30,12 +27,6 @@
         super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_activity)
 
-        Handler(Looper.getMainLooper()).post {
-            val lp: WindowManager.LayoutParams = getWindow().getAttributes()
-            lp.accessibilityTitle = getString(R.string.action_settings)
-            getWindow().setAttributes(lp)
-        }
-
         val toolbar: MaterialToolbar = findViewById(R.id.settings_toolbar)
         setSupportActionBar(toolbar)
         val settingsItems =
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
index 0bcfbea..b4a65cc 100644
--- a/android/TerminalApp/res/layout/activity_headless.xml
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -59,7 +59,7 @@
                 android:layout_width="match_parent"
                 android:layout_height="0dp"
                 android:layout_weight="1" />
-            <include layout="@layout/layout_keyboard" />
+            <include layout="@layout/layout_modifier_keys" />
         </LinearLayout>
     </FrameLayout>
 
diff --git a/android/TerminalApp/res/layout/layout_keyboard.xml b/android/TerminalApp/res/layout/layout_modifier_keys.xml
similarity index 98%
rename from android/TerminalApp/res/layout/layout_keyboard.xml
rename to android/TerminalApp/res/layout/layout_modifier_keys.xml
index d8b7e11..ff0b341 100644
--- a/android/TerminalApp/res/layout/layout_keyboard.xml
+++ b/android/TerminalApp/res/layout/layout_modifier_keys.xml
@@ -17,7 +17,7 @@
 <!--TODO(b/376813452): we might want tablet UI for that-->
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/keyboard_container"
+    android:id="@+id/modifier_keys"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical" >
@@ -79,4 +79,4 @@
             android:id="@+id/btn_pgdn"
             android:text="@string/btn_pgdn_text" />
     </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/android/TerminalApp/res/values-sw720dp/config.xml b/android/TerminalApp/res/values-sw720dp/config.xml
new file mode 100644
index 0000000..be731da
--- /dev/null
+++ b/android/TerminalApp/res/values-sw720dp/config.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <bool name="terminal_portrait_only">false</bool>
+</resources>
diff --git a/android/TerminalApp/res/values/config.xml b/android/TerminalApp/res/values/config.xml
index ea762fc..6440ee6 100644
--- a/android/TerminalApp/res/values/config.xml
+++ b/android/TerminalApp/res/values/config.xml
@@ -19,4 +19,6 @@
     <string name="preference_disk_size_key" translatable="false">PREFERENCE_DISK_SIZE_KEY</string>
     <string name="preference_forwarding_ports" translatable="false">PREFERENCE_FORWARDING_PORTS</string>
     <string name="preference_forwarding_port_is_enabled" translatable="false">PREFERENCE_FORWARDING_PORT_IS_ENABLED_</string>
+
+    <bool name="terminal_portrait_only">true</bool>
 </resources>
diff --git a/build/debian/build.sh b/build/debian/build.sh
index bd918dd..7231a7c 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -56,13 +56,13 @@
 }
 
 prepare_build_id() {
-	local file=${workdir}/build_id
+	local filename=build_id
 	if [ -z "${KOKORO_BUILD_NUMBER}" ]; then
-		echo eng-$(hostname)-$(date --utc) > ${file}
+		echo eng-$(hostname)-$(date --utc) > ${filename}
 	else
-		echo ${KOKOR_BUILD_NUMBER} > ${file}
+		echo ${KOKORO_BUILD_NUMBER} > ${filename}
 	fi
-	echo ${file}
+	echo ${filename}
 }
 
 install_prerequisites() {
diff --git a/guest/pvmfw/src/entry.rs b/guest/pvmfw/src/entry.rs
index ce911b8..48585f3 100644
--- a/guest/pvmfw/src/entry.rs
+++ b/guest/pvmfw/src/entry.rs
@@ -129,7 +129,7 @@
         page_table,
         crosvm::MEM_START..layout::MAX_VIRT_ADDR,
         crosvm::MMIO_RANGE,
-        Some(memory::appended_payload_range()),
+        Some(layout::image_footer_range()),
     ));
 
     let slices = memory::MemorySlices::new(
@@ -189,7 +189,7 @@
 
     const SCTLR_EL1_VAL: u64 = SCTLR_EL1_RES1 | SCTLR_EL1_ITD | SCTLR_EL1_SED | SCTLR_EL1_I;
 
-    let scratch = layout::scratch_range();
+    let scratch = layout::data_bss_range();
 
     assert_ne!(scratch.end - scratch.start, 0, "scratch memory is empty.");
     assert_eq!(scratch.start.0 % ASM_STP_ALIGN, 0, "scratch memory is misaligned.");
@@ -205,6 +205,12 @@
     assert_eq!(stack.start.0 % ASM_STP_ALIGN, 0, "Misaligned stack region.");
     assert_eq!(stack.end.0 % ASM_STP_ALIGN, 0, "Misaligned stack region.");
 
+    let eh_stack = layout::eh_stack_range();
+
+    assert_ne!(eh_stack.end - eh_stack.start, 0, "EH stack region is empty.");
+    assert_eq!(eh_stack.start.0 % ASM_STP_ALIGN, 0, "Misaligned EH stack region.");
+    assert_eq!(eh_stack.end.0 % ASM_STP_ALIGN, 0, "Misaligned EH stack region.");
+
     // Zero all memory that could hold secrets and that can't be safely written to from Rust.
     // Disable the exception vector, caches and page table and then jump to the payload at the
     // given address, passing it the given FDT pointer.
@@ -250,6 +256,18 @@
             "cmp {cache_line}, {stack_end}",
             "b.lo 0b",
 
+            "mov {cache_line}, {eh_stack}",
+            // Zero EH stack region.
+            "0: stp xzr, xzr, [{eh_stack}], 16",
+            "cmp {eh_stack}, {eh_stack_end}",
+            "b.lo 0b",
+
+            // Flush d-cache over EH stack region.
+            "0: dc cvau, {cache_line}",
+            "add {cache_line}, {cache_line}, {dcache_line_size}",
+            "cmp {cache_line}, {eh_stack_end}",
+            "b.lo 0b",
+
             "msr sctlr_el1, {sctlr_el1_val}",
             "isb",
             "mov x1, xzr",
@@ -293,6 +311,8 @@
             scratch_end = in(reg) u64::try_from(scratch.end.0).unwrap(),
             stack = in(reg) u64::try_from(stack.start.0).unwrap(),
             stack_end = in(reg) u64::try_from(stack.end.0).unwrap(),
+            eh_stack = in(reg) u64::try_from(eh_stack.start.0).unwrap(),
+            eh_stack_end = in(reg) u64::try_from(eh_stack.end.0).unwrap(),
             dcache_line_size = in(reg) u64::try_from(min_dcache_line_size()).unwrap(),
             in("x0") fdt_address,
             in("x30") payload_start,
@@ -306,7 +326,7 @@
 /// This must only be called once, since we are returning a mutable reference.
 /// The appended data region must be mapped.
 unsafe fn get_appended_data_slice() -> &'static mut [u8] {
-    let range = memory::appended_payload_range();
+    let range = layout::image_footer_range();
     // SAFETY: This region is mapped and the linker script prevents it from overlapping with other
     // objects.
     unsafe { slice::from_raw_parts_mut(range.start.0 as *mut u8, range.end - range.start) }
diff --git a/guest/pvmfw/src/memory.rs b/guest/pvmfw/src/memory.rs
index 8e8b338..7d49bca 100644
--- a/guest/pvmfw/src/memory.rs
+++ b/guest/pvmfw/src/memory.rs
@@ -30,18 +30,9 @@
 use log::warn;
 use vmbase::{
     layout::{self, crosvm},
-    memory::{PageTable, MEMORY, SIZE_2MB, SIZE_4KB},
-    util::align_up,
+    memory::{PageTable, MEMORY},
 };
 
-/// Returns memory range reserved for the appended payload.
-pub fn appended_payload_range() -> Range<VirtualAddress> {
-    let start = align_up(layout::binary_end().0, SIZE_4KB).unwrap();
-    // pvmfw is contained in a 2MiB region so the payload can't be larger than the 2MiB alignment.
-    let end = align_up(start, SIZE_2MB).unwrap();
-    VirtualAddress(start)..VirtualAddress(end)
-}
-
 /// Region allocated for the stack.
 pub fn stack_range() -> Range<VirtualAddress> {
     const STACK_PAGES: usize = 12;
@@ -54,11 +45,12 @@
 
     // Stack and scratch ranges are explicitly zeroed and flushed before jumping to payload,
     // so dirty state management can be omitted.
-    page_table.map_data(&layout::scratch_range().into())?;
+    page_table.map_data(&layout::data_bss_range().into())?;
+    page_table.map_data(&layout::eh_stack_range().into())?;
     page_table.map_data(&stack_range().into())?;
     page_table.map_code(&layout::text_range().into())?;
     page_table.map_rodata(&layout::rodata_range().into())?;
-    page_table.map_data_dbm(&appended_payload_range().into())?;
+    page_table.map_data_dbm(&layout::image_footer_range().into())?;
     if let Err(e) = page_table.map_device(&layout::console_uart_page().into()) {
         error!("Failed to remap the UART as a dynamic page table entry: {e}");
         return Err(e);
diff --git a/guest/rialto/src/main.rs b/guest/rialto/src/main.rs
index ec26e0f..61e9948 100644
--- a/guest/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -73,7 +73,8 @@
 fn new_page_table() -> Result<PageTable> {
     let mut page_table = PageTable::default();
 
-    page_table.map_data(&layout::scratch_range().into())?;
+    page_table.map_data(&layout::data_bss_range().into())?;
+    page_table.map_data(&layout::eh_stack_range().into())?;
     page_table.map_data(&layout::stack_range(40 * PAGE_SIZE).into())?;
     page_table.map_code(&layout::text_range().into())?;
     page_table.map_rodata(&layout::rodata_range().into())?;
diff --git a/guest/vmbase_example/src/main.rs b/guest/vmbase_example/src/main.rs
index c7ef061..f00effa 100644
--- a/guest/vmbase_example/src/main.rs
+++ b/guest/vmbase_example/src/main.rs
@@ -37,7 +37,10 @@
     bionic, configure_heap,
     fdt::pci::PciInfo,
     generate_image_header,
-    layout::{console_uart_page, crosvm::FDT_MAX_SIZE, rodata_range, scratch_range, text_range},
+    layout::{
+        console_uart_page, crosvm::FDT_MAX_SIZE, data_bss_range, eh_stack_range, rodata_range,
+        text_range,
+    },
     linker, logger, main,
     memory::{PageTable, SIZE_64KB},
 };
@@ -54,7 +57,8 @@
     page_table.map_device(&console_uart_page().into())?;
     page_table.map_code(&text_range().into())?;
     page_table.map_rodata(&rodata_range().into())?;
-    page_table.map_data(&scratch_range().into())?;
+    page_table.map_data(&data_bss_range().into())?;
+    page_table.map_data(&eh_stack_range().into())?;
     page_table.map_data(&boot_stack_range().into())?;
 
     info!("Activating IdMap...");
diff --git a/libs/libvmbase/sections.ld b/libs/libvmbase/sections.ld
index 5ca5ff4..222edae 100644
--- a/libs/libvmbase/sections.ld
+++ b/libs/libvmbase/sections.ld
@@ -56,17 +56,6 @@
 	} >image
 	rodata_end = .;
 
-	.eh_stack (NOLOAD) : ALIGN(4096) {
-		/*
-		 * Get stack overflow guard from the previous page being from
-		 * .rodata and mapped read-only or left unmapped.
-		 */
-		eh_stack_limit = .;
-		. += 4096;
-		. = ALIGN(4096);
-		init_eh_stack_pointer = .;
-	} >writable_data
-
 	/*
 	 * Collect together the read-write data including .bss at the end which
 	 * will be zero'd by the entry code. This is page aligned so it can be
@@ -87,6 +76,13 @@
 	/* Everything beyond this point will not be included in the binary. */
 	bin_end = data_lma + SIZEOF(.data);
 
+	/* Data may be appended at load time to our binary. */
+	.image_footer (NOLOAD) : ALIGN(4096) {
+		image_footer_begin = .;
+		. = ALIGN(LENGTH(image));
+		image_footer_end = .;
+	} >image
+
 	/* The entry point code assumes that .bss is 16-byte aligned. */
 	.bss : ALIGN(16)  {
 		bss_begin = .;
@@ -96,6 +92,18 @@
 		bss_end = .;
 	} >writable_data
 
+	/* Left unmapped, to catch overflows of the exception handler stack. */
+	.eh_stack_guard_page (NOLOAD) : ALIGN(4096) {
+		. += 4096;
+	} >writable_data
+
+	/* Exception handler stack, mapped read-write. */
+	.eh_stack (NOLOAD) : ALIGN(4096) {
+		eh_stack_limit = .;
+		. += 4096;
+		init_eh_stack_pointer = .;
+	} >writable_data
+
 	/* Left unmapped, to catch overflows of the stack. */
 	.stack_guard_page (NOLOAD) : ALIGN(4096) {
 		. += 4096;
diff --git a/libs/libvmbase/src/layout.rs b/libs/libvmbase/src/layout.rs
index adcb2fa..a8f7827 100644
--- a/libs/libvmbase/src/layout.rs
+++ b/libs/libvmbase/src/layout.rs
@@ -70,6 +70,11 @@
     linker_region!(rodata_begin, rodata_end)
 }
 
+/// Region which may contain a footer appended to the binary at load time.
+pub fn image_footer_range() -> Range<VirtualAddress> {
+    linker_region!(image_footer_begin, image_footer_end)
+}
+
 /// Initialised writable data.
 pub fn data_range() -> Range<VirtualAddress> {
     linker_region!(data_begin, data_end)
@@ -80,6 +85,11 @@
     linker_region!(bss_begin, bss_end)
 }
 
+/// Writable data region for .data and .bss.
+pub fn data_bss_range() -> Range<VirtualAddress> {
+    linker_region!(data_begin, bss_end)
+}
+
 /// Writable data region for the stack.
 pub fn stack_range(stack_size: usize) -> Range<VirtualAddress> {
     let end = linker_addr!(init_stack_pointer);
@@ -89,9 +99,9 @@
     start..end
 }
 
-/// All writable sections, excluding the stack.
-pub fn scratch_range() -> Range<VirtualAddress> {
-    linker_region!(eh_stack_limit, bss_end)
+/// Writable data region for the exception handler stack.
+pub fn eh_stack_range() -> Range<VirtualAddress> {
+    linker_region!(eh_stack_limit, init_eh_stack_pointer)
 }
 
 /// Range of the page at UART_PAGE_ADDR of PAGE_SIZE.
diff --git a/libs/libvmbase/src/linker.rs b/libs/libvmbase/src/linker.rs
index 97bef3f..8654cf9 100644
--- a/libs/libvmbase/src/linker.rs
+++ b/libs/libvmbase/src/linker.rs
@@ -35,6 +35,12 @@
     pub static dtb_end: u8;
     /// First byte of the region available for the exception handler stack.
     pub static eh_stack_limit: u8;
+    /// First byte of the `.image_footer` section.
+    pub static image_footer_begin: u8;
+    /// First byte beyond the `.image_footer` section.
+    pub static image_footer_end: u8;
+    /// First byte past the region available for the exception handler stack.
+    pub static init_eh_stack_pointer: u8;
     /// First byte past the region available for the stack.
     pub static init_stack_pointer: u8;
     /// First byte of the `.rodata` section.