diff --git a/boot/piboot/piboot.go b/boot/piboot/piboot.go
index eb0590ab4cf9229842cff21eb0c87f3dd0f42163..5f714fe7a2e0681e1b54eba38ae716229a292094 100644
--- a/boot/piboot/piboot.go
+++ b/boot/piboot/piboot.go
@@ -1,15 +1,17 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-FileCopyrightText: Huawei Inc.
 
-// Package piboot implements the bootloader protocol for the Raspberry Pi boot loader.
+// Package piboot implements the boot protocol for the Raspberry Pi.
 //
 // This implementation assumes the following: The raspberry pi boot firmware is
-// at least the release from 2020-10-28. The kernel image and command line files
-// are stored in sub-directories of the firmware boot partition under slot-a/
-// and slot-b/. Each kernel command line file points to the correct slot. The
-// kernel image supports the RPI_FIRMWARE_SET_REBOOT_FLAGS feature.
+// at least the release from 2020-10-28. All boot assets selected by the
+// os_prefix= mechanism are stored in a sub-directory named either slot-a/ or
+// slot-b/, one for each slot.
 //
-// XXX: simplify slot-a/ and slot-b/ detection with the "prefix" variable.
+// Each kernel command line file points root= argument to block device matching
+// each slot. The kernel image supports the RPI_FIRMWARE_SET_REBOOT_FLAGS
+// feature, which is used to perform "tryboot" reboots before commiting the new
+// configuration.
 //
 // References:
 //  - https://github.com/raspberrypi/linux/commit/757666748ebf69dc161a262faa3717a14d68e5aa
@@ -23,78 +25,71 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strconv"
 	"strings"
 
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/boot"
-	"git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg"
-	"git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/condfilter"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/pimodel"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/rauc/installhandler"
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/squashfstools/unsquashfs"
 )
 
 const (
-	slotAPrefix = "slot-a/"
-	slotBPrefix = "slot-b/"
-
-	// configTxtName is the name of the firmware configuration file read during the boot process.
+	// configTxtName is the name of the firmware configuration file read during
+	// the boot process.
 	configTxtName = "config.txt"
-
-	// trybootTxtName is the name of the firmware configuration file read during the try-once boot process.
-	// It is further described at https://github.com/raspberrypi/linux/commit/757666748ebf69dc161a262faa3717a14d68e5aa
+	// trybootTxtName is the name of the firmware configuration file read during
+	// the try-once boot process. It is further described at
+	// https://github.com/raspberrypi/linux/commit/757666748ebf69dc161a262faa3717a14d68e5aa
 	trybootTxtName = "tryboot.txt"
-
-	kernelParameter    = "kernel"
-	initramfsParameter = "initramfs"
-	cmdlineParameter   = "cmdline"
+	// argSysotaSlot is a kernel command line argument encoding slot name.
+	argSysotaSlot = "sysota.slot="
+	// argRoot is a kenrel command line argument designating the device with the
+	// root file system.
+	argRoot = "root="
 )
 
+func slotPrefix(slot boot.Slot) string {
+	return fmt.Sprintf("slot-%s/", strings.ToLower(slot.String()))
+}
+
 // PiBoot implements boot.Protocol for the Raspberry Pi boot loader.
 type PiBoot struct {
-	// BootMountPoint determines the mount point of the firmware boot partition.
-	// It is expected that the "config.txt" file is present there.
-	BootMountPoint string
-
-	revCode   pimodel.RevisionCode
-	serialNum pimodel.SerialNumber
-
-	// FilterPredicate decides if a config.txt section should be inspected.
-	//
-	// A non-zero filter predicate can be used to support a single system image
-	// that is compatible with different device types, each with a distinct
-	// kernel image by only considering those configuration sections that apply
-	// to the current device.
-	//
-	// When the filter predicate is not used, section names are not parsed and
-	// unrecognized sections are preserved perfectly. When the predicate is set
-	// all sections must be recognized.
-	//
-	// NOTE(zyga): FilterPredicate is deprecated in favour of (upcoming) usage
-	// of revision code and serial number coupled with condfilter.Simulator to
-	// perform read and write requests.
-	FilterPredicate func([]condfilter.ConditionalFilter) bool
+	bootMountPoint string
+	revCode        pimodel.RevisionCode
+	serialNum      pimodel.SerialNumber
 }
 
-// New returns a new PiBoot with the given firmware boot partition mount point.
-// The specified directory must contain the "config.txt" file.
-//
-// Revision code and serial number are stored but are not yet used.
+// New returns a new PiBoot with the given boot mount point and CPU properties.
 func New(bootMountPoint string, revCode pimodel.RevisionCode, serialNum pimodel.SerialNumber) (*PiBoot, error) {
-	file, err := os.Open(filepath.Join(bootMountPoint, configTxtName))
-	if err != nil {
-		return nil, err
-	}
-
-	if err := file.Close(); err != nil {
-		return nil, err
-	}
-
+	// TODO(zyga): drop the error return path if it is not useful later on.
 	return &PiBoot{
-		BootMountPoint: bootMountPoint,
+		bootMountPoint: bootMountPoint,
 		revCode:        revCode,
 		serialNum:      serialNum,
 	}, nil
 }
 
+// BootMountPoint returns the corresponding value passed to the New.
+func (pi *PiBoot) BootMountPoint() string {
+	return pi.bootMountPoint
+}
+
+// RevisionCode returns the corresponding value passed to the New.
+func (pi *PiBoot) RevisionCode() pimodel.RevisionCode {
+	return pi.revCode
+}
+
+// SerialNumber returns the corresponding value passed to the New.
+func (pi *PiBoot) SerialNumber() pimodel.SerialNumber {
+	return pi.serialNum
+}
+
+// activeBootConfig loads the boot config corresponding to the active slot.
+func (pi *PiBoot) activeBootConfig() (*BootConfig, error) {
+	return NewBootConfig(pi.bootMountPoint, pi.revCode, pi.serialNum)
+}
+
 // UnknownActiveSlotError records problems with determining active slot.
 type UnknownActiveSlotError struct {
 	Err error
@@ -126,67 +121,34 @@ func (e *UnknownInactiveSlotError) Error() string {
 }
 
 var (
+	// ErrIncompatible records incompatible boot configuration
+	ErrIncompatible = errors.New("incompatible boot configuration")
 	// ErrCorruptedConfig records boot configuration deviating from assumptions.
 	ErrCorruptedConfig = errors.New("inconsistent or corrupted configuration")
-	// ErrExpectedSlotAOrSlotB records existence of unexpected slot.
-	ErrExpectedSlotAOrSlotB = errors.New("expected slot A or slot B")
 )
 
 // QueryActive returns the slot that is used for booting.
 //
-// Active slot is determined by inspecting the kernel and cmdline parameters in
-// the last section that is applicable, as decided by the filter predicate. By
-// arbitrary convention, both parameters must have the prefix "slot-a/" or
-// "slot-b/" to be recognized.
-//
+// Active slot is determined by the value of os_prefix= parameter.
 // If there is an error, it will be of type *UnknownActiveSlotError.
 func (pi *PiBoot) QueryActive() (slot boot.Slot, err error) {
-	file, err := os.Open(filepath.Join(pi.BootMountPoint, configTxtName))
+	bootCfg, err := pi.activeBootConfig()
 	if err != nil {
 		return boot.InvalidSlot, &UnknownActiveSlotError{Err: err}
 	}
 
-	defer func() {
-		e := file.Close()
-		if err == nil {
-			err = e
-		}
-	}()
-
-	var configTxt picfg.ConfigTxt
-
-	if err := picfg.NewDecoder(file).Decode(&configTxt); err != nil {
-		return boot.InvalidSlot, &UnknownActiveSlotError{Err: err}
-	}
-
-	// Find the configured kernel and kernel cmdline
-	var kernel, initramfs, cmdline string
-
-	err = visitSections(&configTxt, pi.FilterPredicate, func(sect *picfg.Section) error {
-		for _, param := range sect.Parameters {
-			switch param.Name {
-			case kernelParameter:
-				kernel = param.Value
-			case initramfsParameter:
-				initramfs = param.Value
-			case cmdlineParameter:
-				cmdline = param.Value
-			}
-		}
-		return nil
-	})
+	osPrefix, _, err := bootCfg.ComputedOSPrefix()
 	if err != nil {
 		return boot.InvalidSlot, &UnknownActiveSlotError{Err: err}
 	}
-	// Assuming kernel and cmdline are stored in slot-specific sub-directories,
-	// find the configured slot.
-	switch {
-	case strings.HasPrefix(kernel, slotAPrefix) && strings.HasPrefix(cmdline, slotAPrefix) && strings.HasPrefix(initramfs, slotAPrefix):
+
+	switch osPrefix {
+	case slotPrefix(boot.SlotA):
 		return boot.SlotA, nil
-	case strings.HasPrefix(kernel, slotBPrefix) && strings.HasPrefix(cmdline, slotBPrefix) && strings.HasPrefix(initramfs, slotBPrefix):
+	case slotPrefix(boot.SlotB):
 		return boot.SlotB, nil
 	default:
-		return boot.InvalidSlot, &UnknownActiveSlotError{Err: ErrCorruptedConfig}
+		return boot.InvalidSlot, &UnknownActiveSlotError{Err: ErrIncompatible}
 	}
 }
 
@@ -213,77 +175,34 @@ func (pi *PiBoot) QueryInactive() (boot.Slot, error) {
 }
 
 // TrySwitch configures the boot loader for a one-off boot using the given slot.
-func (pi *PiBoot) TrySwitch(to boot.Slot) (err error) {
-	// Load and decode config.txt
-	configTxtFile, err := os.Open(filepath.Join(pi.BootMountPoint, configTxtName))
-	if err != nil {
-		return err
-	}
-
-	defer func() {
-		e := configTxtFile.Close()
-		if err == nil {
-			err = e
-		}
-	}()
-
-	var configTxt picfg.ConfigTxt
-	if err := picfg.NewDecoder(configTxtFile).Decode(&configTxt); err != nil {
-		return err
-	}
-
-	// Find the values to modify, swapping A and B around
-	err = visitSections(&configTxt, pi.FilterPredicate, func(sect *picfg.Section) error {
-		for paramIdx, param := range sect.Parameters {
-			switch param.Name {
-			case kernelParameter, cmdlineParameter, initramfsParameter:
-				var oldPrefix, newPrefix string
-				switch to {
-				case boot.SlotA:
-					oldPrefix = slotBPrefix
-					newPrefix = slotAPrefix
-				case boot.SlotB:
-					oldPrefix = slotAPrefix
-					newPrefix = slotBPrefix
-				default:
-					continue
-				}
-				sect.Parameters[paramIdx].Value = strings.Replace(param.Value, oldPrefix, newPrefix, -1)
-			}
-		}
-		return nil
-	})
+func (pi *PiBoot) TrySwitch(targetSlot boot.Slot) (err error) {
+	slottedBootDir := slotPrefix(targetSlot)
+
+	// Load boot configuration pretending that slot sub-directory of the boot
+	// partition is the is the real boot partition. This is the state we get
+	// from the pre-install handler.
+	slotBootCfg, err := NewBootConfig(
+		filepath.Join(pi.bootMountPoint, slottedBootDir), pi.revCode, pi.serialNum)
 	if err != nil {
 		return err
 	}
 
-	// Encode and write modified config.txt as tryboot.txt
-	trybootTxtFile, err := os.OpenFile(
-		filepath.Join(pi.BootMountPoint, trybootTxtName),
-		os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
-	if err != nil {
+	// Set the os_prefix= to slot-$inactive/ and add a sysota.slot={A,B} flag to
+	// kernel command line (informative only).
+	if err := slotBootCfg.SetOSPrefix(slottedBootDir); err != nil {
 		return err
 	}
 
-	defer func() {
-		e := trybootTxtFile.Close()
-		if err == nil {
-			err = e
-		}
-	}()
-
-	if err := picfg.NewEncoder(trybootTxtFile).Encode(configTxt); err != nil {
-		return err
-	}
+	// Save the configuration as "tryboot.txt" in the real boot mount point.
+	slotBootCfg.BootMountPoint = pi.bootMountPoint
+	slotBootCfg.ConfigFileName = trybootTxtName
 
-	return nil
+	return slotBootCfg.Save()
 }
 
 // CommitSwitch re-configures a try-mode slot for continuous use.
 func (pi *PiBoot) CommitSwitch() error {
-	return os.Rename(
-		filepath.Join(pi.BootMountPoint, trybootTxtName),
-		filepath.Join(pi.BootMountPoint, configTxtName))
+	return os.Rename(filepath.Join(pi.bootMountPoint, trybootTxtName), filepath.Join(pi.bootMountPoint, configTxtName))
 }
 
 const rebootCommand = "reboot"
@@ -298,45 +217,115 @@ func (pi *PiBoot) Reboot(flags boot.RebootFlags) error {
 	return cmd.Run()
 }
 
-// CancelSwitch removes the try-mode boot configuration file.
+// CancelSwitch removes the try-mode boot configuration and cmdline file.
 func (pi *PiBoot) CancelSwitch() error {
-	if err := os.Remove(filepath.Join(pi.BootMountPoint, trybootTxtName)); err != nil && !os.IsNotExist(err) {
+	if err := os.Remove(filepath.Join(pi.bootMountPoint, trybootTxtName)); err != nil && !os.IsNotExist(err) {
 		return err
 	}
 
+	// TODO(zyga): perhaps remove the slot subdirectory as well?
 	return nil
 }
 
-// visitSection calls visitor function on each section in the config file.
-//
-// An optional predicate function may be used to visit only matching sections.
-// The predicate is called with the effective fiters that are in effect at each
-// phase of the traversal process. Sections can be mutated by the visitor.
-func visitSections(configTxt *picfg.ConfigTxt, pred func([]condfilter.ConditionalFilter) bool, visitor func(sect *picfg.Section) error) error {
-	var tracker condfilter.StateTracker
-
-	for idx := range configTxt.Sections {
-		sect := &configTxt.Sections[idx]
-
-		if pred != nil && sect.Filter != "" {
-			filter, err := condfilter.Unmarshal([]byte(sect.Filter))
-			if err != nil {
-				return err
-			}
-
-			tracker.Filter(filter)
-
-			if !pred(tracker.EffectiveFilters()) {
-				continue
-			}
-		}
+// systemClass is the class of the RAUC image with a SystemOTA root file system + boot partition + kernel image.
+const systemClass = "system"
 
-		if err := visitor(sect); err != nil {
-			return err
-		}
+// ErrWouldClobber reports cases where the pre-install hook would clobber active slot.
+var ErrWouldClobber = errors.New("pre-install hook would clobber active slot")
+
+// PreInstallHandler copies boot assets out of system.img and into the boot partition.
+func (pi *PiBoot) PreInstallHandler(ctx *installhandler.HandlerContext) (err error) {
+	// If the handler is invoked but doesn't find a bundle and a target slot
+	// with "system" class then do nothing without reporting an error. This will
+	// allow systems to use other slots and classes without SystemOTA
+	// interfering with that.
+	systemImgInfo := ctx.ImageWithClass(systemClass)
+	targetSlotInfo := ctx.TargetSlotWithClass(systemClass)
+
+	if systemImgInfo == nil || targetSlotInfo == nil {
+		return nil
 	}
 
-	return nil
+	// Which slot are we targetting?
+	var targetSlot boot.Slot
+	if err := targetSlot.UnmarshalText([]byte(targetSlotInfo.BootName)); err != nil {
+		return err
+	}
+
+	// Sanity check, ensure that we never clobber the active slot. This should
+	// never happen but it's just easy to check so let's check and have less
+	// things to worry about.
+	activeSlot, err := pi.QueryActive()
+	if err != nil {
+		return err
+	}
+
+	if activeSlot == targetSlot {
+		return ErrWouldClobber
+	}
+
+	slottedBootDir := slotPrefix(targetSlot)
+
+	// Remove $boot/{squashfs-root/ and $boot/slot-$inactive to make space for
+	// the unpack/move we are about to do below.
+	if err := os.RemoveAll(filepath.Join(pi.bootMountPoint, slottedBootDir)); err != nil {
+		return err
+	}
+
+	const squashfsRootDir = "squashfs-root"
+	if err := os.RemoveAll(filepath.Join(pi.bootMountPoint, squashfsRootDir)); err != nil {
+		return err
+	}
+
+	// Extract the ($system.img)/boot directory from the system.img squashfs
+	// into $boot/squashfs-root/boot.
+	const bootDir = "boot"
+
+	unsquashfsFrom := filepath.Join(ctx.BundleMountPoint, systemImgInfo.Name)
+	unsquashfsTo := filepath.Join(pi.bootMountPoint, squashfsRootDir)
+	unsquashfsWhat := bootDir
+
+	if ctx.InitialHandlerPID != 0 {
+		// Note that the rauc-pre-pre-handler is invoked by RAUC inside a separate
+		// mount namespace where the bundle file is mounted. Look at the mounted
+		// file through the magic symlink in /proc/[pid]/root.
+		unsquashfsFrom = filepath.Join("/proc", strconv.Itoa(ctx.InitialHandlerPID), "root", unsquashfsFrom)
+	}
+
+	if err := unsquashfs.Run(unsquashfsFrom, unsquashfsTo, &unsquashfs.Options{Force: true}, unsquashfsWhat); err != nil {
+		return err
+	}
+
+	// Rename the extracted boot directory to $boot/slot-$inactive.
+	renameFrom := filepath.Join(pi.bootMountPoint, squashfsRootDir, bootDir)
+	renameTo := filepath.Join(pi.bootMountPoint, slottedBootDir)
+
+	if err := os.Rename(renameFrom, renameTo); err != nil {
+		return err
+	}
+
+	// Remove the now-empty squashfs directory.
+	if err := os.RemoveAll(filepath.Join(pi.bootMountPoint, squashfsRootDir)); err != nil {
+		return err
+	}
+
+	// Load the boot configuration from the slot directory and modify root=.
+	bootCfgDir := filepath.Join(pi.bootMountPoint, slotPrefix(targetSlot))
+
+	slotBootCfg, err := NewBootConfig(bootCfgDir, pi.revCode, pi.serialNum)
+	if err != nil {
+		return err
+	}
+
+	// Set the root= argument to the block device matching the slot.
+	slotBootCfg.CmdLine.SetArg(argRoot, targetSlotInfo.DevicePath)
+	// Set the sysota.slot={A,B} kernel command line (informative only).
+	slotBootCfg.CmdLine.SetArg(argSysotaSlot, targetSlot.String())
+
+	// TODO(zyga): validate the final configuration.
+
+	// Save the configuration back to the per-slot directory.
+	return slotBootCfg.Save()
 }
 
 // PostInstallHandler reboots the Raspberry Pi if necessary.
@@ -354,7 +343,7 @@ func (pi *PiBoot) PostInstallHandler(ctx *installhandler.HandlerContext) error {
 // shouldReboot returns true if a slot with "system" class is being modified.
 func shouldReboot(ctx *installhandler.HandlerContext) bool {
 	for _, slotID := range ctx.TargetSlotIDs {
-		if ctx.Slots[slotID].Class == "system" {
+		if ctx.Slots[slotID].Class == systemClass {
 			return true
 		}
 	}
diff --git a/boot/piboot/piboot_test.go b/boot/piboot/piboot_test.go
index 49ba2cbc7879f5aec0df390d03b7dea919aa5434..d301c5cf638f8eeccce82a685445fe5e2f778487 100644
--- a/boot/piboot/piboot_test.go
+++ b/boot/piboot/piboot_test.go
@@ -4,6 +4,7 @@
 package piboot_test
 
 import (
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -13,9 +14,9 @@ import (
 
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/boot"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/boot/piboot"
-	"git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/condfilter"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/picfg/pimodel"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/rauc/installhandler"
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/squashfstools/mksquashfs"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/testutil"
 )
 
@@ -26,6 +27,7 @@ type bootTest struct {
 	bootDir   string
 	revCode   pimodel.RevisionCode
 	serialNum pimodel.SerialNumber
+	pi        *piboot.PiBoot
 }
 
 var _ = Suite(&bootTest{
@@ -36,52 +38,53 @@ var _ = Suite(&bootTest{
 
 func (s *bootTest) SetUpTest(c *C) {
 	s.bootDir = c.MkDir()
-	s.MockConfigTxt(c, "")
+	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
+	c.Assert(err, IsNil)
+
+	s.pi = pi
 }
 
-func (s *bootTest) MockConfigTxt(c *C, text string) {
-	configName := filepath.Join(s.bootDir, "config.txt")
-	err := ioutil.WriteFile(configName, []byte(text), 0o644)
+func (s *bootTest) MockBootFile(c *C, path, content string) {
+	name := filepath.Join(s.bootDir, path)
+
+	err := os.MkdirAll(filepath.Dir(name), 0o755)
 	c.Assert(err, IsNil)
-}
 
-func (s *bootTest) MockTrybootTxt(c *C, text string) {
-	configName := filepath.Join(s.bootDir, "tryboot.txt")
-	err := ioutil.WriteFile(configName, []byte(text), 0o644)
+	err = ioutil.WriteFile(name, []byte(content), 0o644)
 	c.Assert(err, IsNil)
 }
 
-func (s *bootTest) TestOpen(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
-	c.Check(pi.BootMountPoint, Equals, s.bootDir)
+func (s *bootTest) MockConfigTxt(c *C, content string) {
+	s.MockBootFile(c, "config.txt", content)
 }
 
-func (s *bootTest) TestQueryActive(c *C) {
+func (s *bootTest) MockTrybootTxt(c *C, content string) {
+	s.MockBootFile(c, "tryboot.txt", content)
+}
+
+func (s *bootTest) TestNew(c *C) {
 	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
 	c.Assert(err, IsNil)
 
-	s.MockConfigTxt(c, ``)
+	c.Check(pi.BootMountPoint(), Equals, s.bootDir)
+	c.Check(pi.RevisionCode(), Equals, s.revCode)
+	c.Check(pi.SerialNumber(), Equals, s.serialNum)
+}
 
-	slot, err := pi.QueryActive()
-	c.Assert(err, ErrorMatches, "cannot determine active slot: inconsistent or corrupted configuration")
+func (s *bootTest) TestQueryActive(c *C) {
+	slot, err := s.pi.QueryActive()
+	c.Assert(err, ErrorMatches, "cannot determine active slot: incompatible boot configuration")
 	c.Check(slot, Equals, boot.InvalidSlot)
 
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-a/cmdline.txt\n"+
-		"initramfs slot-a/initrd.img followkernel\n")
+	s.MockConfigTxt(c, "os_prefix=slot-a/\n")
 
-	slot, err = pi.QueryActive()
+	slot, err = s.pi.QueryActive()
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, boot.SlotA)
 
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-b/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-b/initrd.img followkernel\n")
+	s.MockConfigTxt(c, "os_prefix=slot-b/\n")
 
-	slot, err = pi.QueryActive()
+	slot, err = s.pi.QueryActive()
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, boot.SlotB)
 
@@ -90,228 +93,145 @@ func (s *bootTest) TestQueryActive(c *C) {
 		"cmdline=slot-b/cmdline.txt\n"+
 		"initramfs slot-c/initrd.img followkernel\n")
 
-	slot, err = pi.QueryActive()
-	c.Assert(err, ErrorMatches, "cannot determine active slot: inconsistent or corrupted configuration")
+	slot, err = s.pi.QueryActive()
+	c.Assert(err, ErrorMatches, "cannot determine active slot: incompatible boot configuration")
 	c.Check(slot, Equals, boot.InvalidSlot)
 }
 
-func (s *bootTest) TestQueryActiveWithFilterPredicate(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
-
-	s.MockConfigTxt(c, ""+
-		"[pi3]\n"+
-		"kernel=slot-b/pi3-vmlinuz\n"+
-		"cmdline=slot-b/pi3-cmdline.txt\n"+
-		"initramfs slot-b/pi3-initrd.img followkernel\n"+
-		"[pi4]\n"+
-		"kernel=slot-a/pi4-vmlinuz\n"+
-		"cmdline=slot-a/pi4-cmdline.txt\n"+
-		"initramfs slot-a/pi4-initrd.img followkernel\n")
-
-	pi.FilterPredicate = func(effective []condfilter.ConditionalFilter) bool {
-		for _, filter := range effective {
-			if model, ok := filter.(condfilter.PiModelFilter); ok {
-				return model == condfilter.Pi4
-			}
-		}
-
-		return true
-	}
-	slot, err := pi.QueryActive()
-	c.Assert(err, IsNil)
-	c.Check(slot, Equals, boot.SlotA)
-
-	pi.FilterPredicate = func(effective []condfilter.ConditionalFilter) bool {
-		for _, filter := range effective {
-			if model, ok := filter.(condfilter.PiModelFilter); ok {
-				return model == condfilter.Pi3
-			}
-		}
-
-		return true
-	}
-	slot, err = pi.QueryActive()
-	c.Assert(err, IsNil)
-	c.Check(slot, Equals, boot.SlotB)
-}
-
 func (s *bootTest) TestQueryInactive(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
-
-	s.MockConfigTxt(c, ``)
-
-	slot, err := pi.QueryInactive()
-	c.Assert(err, ErrorMatches, "cannot determine inactive slot: inconsistent or corrupted configuration")
+	slot, err := s.pi.QueryInactive()
+	c.Assert(err, ErrorMatches, "cannot determine inactive slot: incompatible boot configuration")
 	c.Check(slot, Equals, boot.InvalidSlot)
 
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-a/cmdline.txt\n"+
-		"initramfs slot-a/initrd.img followkernel\n")
+	s.MockConfigTxt(c, "os_prefix=slot-a/")
 
-	slot, err = pi.QueryInactive()
+	slot, err = s.pi.QueryInactive()
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, boot.SlotB)
 
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-b/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-b/initrd.img followkernel\n")
+	s.MockConfigTxt(c, "os_prefix=slot-b/")
 
-	slot, err = pi.QueryInactive()
+	slot, err = s.pi.QueryInactive()
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, boot.SlotA)
 
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-c/initrd.img followkernel\n")
+	s.MockConfigTxt(c, "os_prefix=potato/")
 
-	slot, err = pi.QueryInactive()
-	c.Assert(err, ErrorMatches, "cannot determine inactive slot: inconsistent or corrupted configuration")
+	slot, err = s.pi.QueryInactive()
+	c.Assert(err, ErrorMatches, "cannot determine inactive slot: incompatible boot configuration")
 	c.Check(slot, Equals, boot.InvalidSlot)
 }
 
 func (s *bootTest) TestTrySwitchAtoB(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
-
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-a/cmdline.txt\n"+
-		"initramfs slot-a/initrd.img followkernel\n")
+	s.MockConfigTxt(c, "# pristine")
+	s.MockBootFile(c, "slot-b/config.txt", ""+
+		"kernel=vmlinuz\n"+
+		"cmdline=cmdline.txt\n"+
+		"initramfs initrd.img followkernel\n")
+	s.MockBootFile(c, "slot-b/cmdline.txt", "... root=/dev/mmcblk0p4 sysota.slot=B")
 
-	err = pi.TrySwitch(boot.SlotB)
+	err := s.pi.TrySwitch(boot.SlotB)
 	c.Assert(err, IsNil)
 
+	// The config.txt file is not touched.
 	data, err := ioutil.ReadFile(filepath.Join(s.bootDir, "config.txt"))
 	c.Assert(err, IsNil)
-	c.Check(string(data), Equals, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-a/cmdline.txt\n"+
-		"initramfs slot-a/initrd.img followkernel\n")
+	c.Check(string(data), Equals, "# pristine")
 
+	// The tryboot.txt is slot-b/config.txt with os_prefix= set.
 	data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "tryboot.txt"))
 	c.Assert(err, IsNil)
 	c.Check(string(data), Equals, ""+
-		"kernel=slot-b/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-b/initrd.img followkernel\n")
-}
+		"kernel=vmlinuz\n"+
+		"cmdline=cmdline.txt\n"+
+		"initramfs initrd.img followkernel\n"+
+		"os_prefix=slot-b/\n")
 
-func (s *bootTest) TestTrySwitchBtoA(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
+	// The cmdline.txt file is not modified.
+	data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "cmdline.txt"))
 	c.Assert(err, IsNil)
+	c.Check(string(data), Equals, "... root=/dev/mmcblk0p4 sysota.slot=B")
+}
 
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-b/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-b/initrd.img followkernel\n")
+func (s *bootTest) TestTrySwitchBtoA(c *C) {
+	s.MockConfigTxt(c, "# pristine")
+	s.MockBootFile(c, "slot-a/config.txt", ""+
+		"kernel=vmlinuz\n"+
+		"cmdline=cmdline.txt\n"+
+		"initramfs initrd.img followkernel\n")
+	s.MockBootFile(c, "slot-a/cmdline.txt", "... root=/dev/mmcblk0p3 sysota.slot=A")
 
-	err = pi.TrySwitch(boot.SlotA)
+	err := s.pi.TrySwitch(boot.SlotA)
 	c.Assert(err, IsNil)
 
+	// The config.txt file is not touched.
 	data, err := ioutil.ReadFile(filepath.Join(s.bootDir, "config.txt"))
 	c.Assert(err, IsNil)
-	c.Check(string(data), Equals, ""+
-		"kernel=slot-b/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-b/initrd.img followkernel\n")
+	c.Check(string(data), Equals, "# pristine")
 
+	// The tryboot.txt is slot-a/config.txt with os_prefix= set.
 	data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "tryboot.txt"))
 	c.Assert(err, IsNil)
 	c.Check(string(data), Equals, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-a/cmdline.txt\n"+
-		"initramfs slot-a/initrd.img followkernel\n")
-}
+		"kernel=vmlinuz\n"+
+		"cmdline=cmdline.txt\n"+
+		"initramfs initrd.img followkernel\n"+
+		"os_prefix=slot-a/\n")
 
-func (s *bootTest) TestReboot(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
+	// The cmdline.txt file is not modified.
+	data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-a", "cmdline.txt"))
 	c.Assert(err, IsNil)
+	c.Check(string(data), Equals, "... root=/dev/mmcblk0p3 sysota.slot=A")
+}
 
+func (s *bootTest) TestReboot(c *C) {
 	restore, rebootLog := s.MockCommand(c, "reboot")
 	defer restore()
 
-	err = pi.Reboot(0)
+	err := s.pi.Reboot(0)
 	c.Assert(err, IsNil)
 	c.Check(rebootLog(c), DeepEquals, []string{`reboot`})
 
-	err = pi.Reboot(boot.RebootTryBoot)
+	err = s.pi.Reboot(boot.RebootTryBoot)
 	c.Assert(err, IsNil)
 	c.Check(rebootLog(c), DeepEquals, []string{`reboot 0\ tryboot`})
 }
 
 func (s *bootTest) TestCommitSwitch(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
+	s.MockConfigTxt(c, "os_prefix=slot-a/")
+	s.MockTrybootTxt(c, "os_prefix=slot-b/")
 
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-a/cmdline.txt\n"+
-		"initramfs slot-a/initrd.img followkernel\n")
-	s.MockTrybootTxt(c, ""+
-		"kernel=slot-b/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-b/initrd.img followkernel\n")
-
-	err = pi.CommitSwitch()
+	err := s.pi.CommitSwitch()
 	c.Assert(err, IsNil)
 
-	slot, err := pi.QueryActive()
+	slot, err := s.pi.QueryActive()
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, boot.SlotB)
 
-	_, err = os.Stat(filepath.Join(pi.BootMountPoint, "tryboot.txt"))
+	_, err = os.Stat(filepath.Join(s.bootDir, "tryboot.txt"))
 	c.Assert(os.IsNotExist(err), Equals, true)
 }
 
 func (s *bootTest) TestCancelSwitch(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
-
-	s.MockConfigTxt(c, ""+
-		"kernel=slot-a/vmlinuz\n"+
-		"cmdline=slot-a/cmdline.txt\n"+
-		"initramfs slot-a/initrd.img followkernel\n")
-	s.MockTrybootTxt(c, ""+
-		"kernel=slot-b/vmlinuz\n"+
-		"cmdline=slot-b/cmdline.txt\n"+
-		"initramfs slot-b/initrd.img followkernel\n")
+	s.MockConfigTxt(c, "os_prefix=slot-a/")
+	s.MockTrybootTxt(c, "os_prefix=slot-b/")
 
-	err = pi.CancelSwitch()
+	err := s.pi.CancelSwitch()
 	c.Assert(err, IsNil)
 
-	slot, err := pi.QueryActive()
+	slot, err := s.pi.QueryActive()
 	c.Assert(err, IsNil)
 	c.Check(slot, Equals, boot.SlotA)
 
-	_, err = os.Stat(filepath.Join(pi.BootMountPoint, "tryboot.txt"))
+	_, err = os.Stat(filepath.Join(s.pi.BootMountPoint(), "tryboot.txt"))
 	c.Assert(os.IsNotExist(err), Equals, true)
 
 	// Idempotent
-	err = pi.CancelSwitch()
+	err = s.pi.CancelSwitch()
 	c.Assert(err, IsNil)
 }
 
-func (s *bootTest) TestVisitSectionsCondfilterErrors(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
-
-	pi.FilterPredicate = func([]condfilter.ConditionalFilter) bool { return true }
-
-	s.MockConfigTxt(c, "[frozbonicator]")
-
-	_, err = pi.QueryActive()
-	c.Assert(err, ErrorMatches, `cannot determine active slot: conditional filter error "\[frozbonicator\]": unknown filter`)
-}
-
 func (s *bootTest) TestPostInstallHandler(c *C) {
-	pi, err := piboot.New(s.bootDir, s.revCode, s.serialNum)
-	c.Assert(err, IsNil)
-
 	restore, rebootLog := s.MockCommand(c, "reboot")
 	defer restore()
 
@@ -321,7 +241,7 @@ func (s *bootTest) TestPostInstallHandler(c *C) {
 		"RAUC_SLOT_CLASS_1=potato",
 	})
 	c.Check(err, IsNil)
-	err = pi.PostInstallHandler(ctx)
+	err = s.pi.PostInstallHandler(ctx)
 	c.Check(err, IsNil)
 	c.Check(rebootLog(c), DeepEquals, []string(nil))
 
@@ -331,7 +251,97 @@ func (s *bootTest) TestPostInstallHandler(c *C) {
 		"RAUC_SLOT_CLASS_1=system",
 	})
 	c.Assert(err, IsNil)
-	err = pi.PostInstallHandler(ctx)
+	err = s.pi.PostInstallHandler(ctx)
 	c.Check(err, IsNil)
 	c.Check(rebootLog(c), DeepEquals, []string{`reboot 0\ tryboot`})
 }
+
+type preInstallHandlerSuite struct {
+	bootDir   string
+	bundleDir string
+	systemDir string
+
+	revCode   pimodel.RevisionCode
+	serialNum pimodel.SerialNumber
+
+	pi  *piboot.PiBoot
+	ctx *installhandler.HandlerContext
+}
+
+var _ = Suite(&preInstallHandlerSuite{
+	// Arbitrary choice for as long a valid revision is used.
+	revCode:   Pi4BRevCode,
+	serialNum: sampleSerial,
+})
+
+func (s *preInstallHandlerSuite) SetUpTest(c *C) {
+	var err error
+
+	s.bootDir = c.MkDir()
+	s.bundleDir = c.MkDir()
+	s.systemDir = c.MkDir()
+
+	s.pi, err = piboot.New(s.bootDir, s.revCode, s.serialNum)
+	c.Assert(err, IsNil)
+
+	// Prepare minimum configuration of the current boot system.
+	// This is needed to detect updates which would clobber the current system.
+	c.Assert(ioutil.WriteFile(filepath.Join(s.bootDir, "config.txt"), []byte("os_prefix=slot-a/"), 0o600), IsNil)
+
+	// Prepare an fake system image.
+	c.Assert(os.Mkdir(filepath.Join(s.systemDir, "boot"), 0o700), IsNil)
+	c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/vmlinuz-5.10"), []byte("kernel"), 0o600), IsNil)
+	c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/initrd-5.10.img"), []byte("initrd"), 0o600), IsNil)
+	c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/config.txt"), []byte(""+
+		"kernel=vmlinuz-5.10\n"+
+		"initramfs initrd-5.10.img followkernel\n"), 0o600), IsNil)
+	c.Assert(ioutil.WriteFile(filepath.Join(s.systemDir, "boot/cmdline.txt"), []byte("root=hardcoded"), 0o600), IsNil)
+	c.Assert(mksquashfs.Run(filepath.Join(s.bundleDir, "system.img"), nil, s.systemDir), IsNil)
+
+	// Prepare the handler context
+	s.ctx, err = installhandler.NewHandlerContext([]string{
+		fmt.Sprintf("RAUC_BUNDLE_MOUNT_POINT=%s", s.bundleDir),
+		"RAUC_IMAGE_CLASS_1=system",
+		"RAUC_IMAGE_NAME_1=system.img",
+		"RAUC_SLOT_BOOTNAME_1=B",
+		"RAUC_SLOT_BOOTNAME_2=A",
+		"RAUC_SLOT_CLASS_1=system",
+		"RAUC_SLOT_CLASS_2=system",
+		"RAUC_SLOT_DEVICE_1=/dev/mmcblk0p4",
+		"RAUC_SLOT_DEVICE_2=/dev/mmcblk0p3",
+		"RAUC_SLOTS=1 2 ",
+		"RAUC_TARGET_SLOTS=1 ",
+	})
+	c.Assert(err, IsNil)
+}
+
+func (s *preInstallHandlerSuite) TestPreInstallHandler(c *C) {
+	// Run the pre-install handler.
+	err := s.pi.PreInstallHandler(s.ctx)
+	c.Assert(err, IsNil)
+
+	data, err := ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "config.txt"))
+	c.Assert(err, IsNil)
+	c.Check(string(data), Equals, ""+
+		"kernel=vmlinuz-5.10\n"+
+		"initramfs initrd-5.10.img followkernel\n")
+
+	data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "cmdline.txt"))
+	c.Assert(err, IsNil)
+	c.Check(string(data), Equals, "root=/dev/mmcblk0p4 sysota.slot=B")
+
+	data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "vmlinuz-5.10"))
+	c.Assert(err, IsNil)
+	c.Check(string(data), Equals, "kernel")
+
+	data, err = ioutil.ReadFile(filepath.Join(s.bootDir, "slot-b", "initrd-5.10.img"))
+	c.Assert(err, IsNil)
+	c.Check(string(data), Equals, "initrd")
+}
+
+func (s *preInstallHandlerSuite) TestPreInstallHandlerSanityCheck(c *C) {
+	c.Assert(ioutil.WriteFile(filepath.Join(s.bootDir, "config.txt"), []byte("os_prefix=slot-b/"), 0o600), IsNil)
+
+	err := s.pi.PreInstallHandler(s.ctx)
+	c.Assert(err, ErrorMatches, `pre-install hook would clobber active slot`)
+}
diff --git a/boot/piboot/tests/bin/check-slot-a-b-pristine.sh b/boot/piboot/tests/bin/check-slot-a-b-pristine.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c8b4cf1815a2aab19136d2b0972f25e05c62b4ed
--- /dev/null
+++ b/boot/piboot/tests/bin/check-slot-a-b-pristine.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Huawei Inc.
+
+set -e
+set -x
+
+# Check that the slot-specific boot directories originally copied by the
+# pre-install handler are pristine. This data is consistent with what is
+# prepared by spread.yaml for the test suite boot/piboot/tests.
+
+test -e "$SYSOTA_BOOT_DIR"/slot-a/config.txt
+test -e "$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt
+
+test -e "$SYSOTA_BOOT_DIR"/slot-b/config.txt
+test -e "$SYSOTA_BOOT_DIR"/slot-b/cmdline.txt
+
+# OS prefix is not set in either config file.
+NOMATCH 'os_prefix=' "$SYSOTA_BOOT_DIR"/slot-a/config.txt
+NOMATCH 'os_prefix=' "$SYSOTA_BOOT_DIR"/slot-b/config.txt
+
+# cmdline= is set to the data provided by RAUC.
+MATCH 'root=/tmp/rauc-fake-system/slot-a' "$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt
+MATCH 'root=/tmp/rauc-fake-system/slot-b' "$SYSOTA_BOOT_DIR"/slot-b/cmdline.txt
diff --git a/boot/piboot/tests/bundle-install/task.yaml b/boot/piboot/tests/bundle-install/task.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0d0b2d0baf13a864910fe21cdbd7616a49138804
--- /dev/null
+++ b/boot/piboot/tests/bundle-install/task.yaml
@@ -0,0 +1,89 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Huawei Inc.
+
+summary: play-through of "rauc install"
+# This test doesn't run in LXD as RAUC cannot  mount a squashfs due to the
+# sandbox blocking that. It would only work if RAUC would be able to detect
+# and handle that situation and switch to FUSE. This test works fine in qemu.
+backends: [-lxd]
+prepare: |
+    mkdir bundle-dir
+    mkdir system-dir
+
+    # Create a fake system image, with an extremely barren /etc and /boot
+    # directories. As the validation logic in SysOTA improves, this may have to
+    # look more realistic, at least with regards to boot assets.
+    mkdir system-dir/etc
+    mkdir system-dir/boot
+
+    cat <<OS_RELEASE > system-dir/etc/os-release
+    ID=sysota-test
+    ID_VERSION=1
+    OS_RELEASE
+
+    cat <<CONFIG_TXT > system-dir/boot/config.txt
+    kernel=new-kernel.img
+    CONFIG_TXT
+
+    cat <<CMDLINE_TXT > system-dir/boot/cmdline.txt
+    new-cmdline-text root=hardcoded-root
+    CMDLINE_TXT
+
+    mksquashfs system-dir bundle-dir/system.img -comp zstd
+
+    # APPEND to the RAUC configuration file to set SystemOTA as custom
+    # bootloader handler, pre-install handler and post-install handler.
+    cat <<RAUC_SYSTEM_CONF >>/etc/rauc/system.conf
+    [handlers]
+    pre-install=/usr/libexec/sysota/rauc-pre-install-handler
+    post-install=/usr/libexec/sysota/rauc-post-install-handler
+    bootloader-custom-backend=/usr/libexec/sysota/rauc-custom-boot-handler
+    RAUC_SYSTEM_CONF
+
+    # Create a fake rauc bundle with that system image
+    cat <<RAUC_MANIFEST >bundle-dir/manifest.raucm
+    [update]
+    compatible=OSTC SystemOTA Test Environment
+    version=1
+
+    [image.system]
+    filename=system.img
+    RAUC_MANIFEST
+
+    rauc bundle --cert="$TEST_RAUC_KEYS_DIR/cert.pem" --key="$TEST_RAUC_KEYS_DIR/key.pem" bundle-dir bundle.img
+
+    # Store /proc/cmdline and allow us to fake /proc/cmdline easily in the execute phase below.
+    cp /proc/cmdline cmdline.orig
+    cp cmdline.orig cmdline
+    printf "%s rauc.slot=A\n" "$(cat cmdline.orig)" > cmdline
+    mount --bind cmdline /proc/cmdline
+
+    # Stop rauc and grab a cursor for the journal log
+    systemctl stop rauc.service || true
+    journalctl -u sysotad.service --cursor-file=cursor >/dev/null || true
+
+    # Truncate the log files log and reset backend state
+    truncate --size=0 /var/{backend,pre-install,post-install}.log
+    echo 0 >/tmp/backend.state
+
+execute: |
+    rauc install bundle.img
+
+    MATCH "os_prefix=slot-b/" "$SYSOTA_BOOT_DIR/tryboot.txt"
+    MATCH "kernel=new-kernel.img" "$SYSOTA_BOOT_DIR/tryboot.txt"
+    MATCH "new-cmdline-text root=/tmp/rauc-fake-system/slot-b sysota.slot=B" "$SYSOTA_BOOT_DIR/slot-b/cmdline.txt"
+
+    test -f "$MOCK_REBOOT_LOG"
+    MATCH '^/usr/sbin/reboot 0\\ tryboot$' "$MOCK_REBOOT_LOG"
+
+restore: |
+    umount /proc/cmdline || true
+
+    rm -rf system-dir bundle-dir
+    rm -f *.img
+    rm -f cursor
+    rm -f cmdline{,.orig}
+
+debug: |
+    cat /proc/cmdline
+    journalctl --cursor-file=cursor -u rauc.service
diff --git a/boot/piboot/tests/cancel-switch/task.yaml b/boot/piboot/tests/cancel-switch/task.yaml
index 74423f208d0fa5f99739a1dee7c71e64b7dc1d9c..173bcd79182942e2194f82bd5d51503612d3f7c6 100644
--- a/boot/piboot/tests/cancel-switch/task.yaml
+++ b/boot/piboot/tests/cancel-switch/task.yaml
@@ -5,16 +5,21 @@ summary: Integration tests for the PiBoot.CancelSwitch function
 details: |
     The CancelSwitch function removes the tryboot.txt file.
 prepare: |
-    cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/tryboot.txt
-    kernel=slot-b/kernel
-    cmdline=slot-b/cmdline.txt
-    initramfs slot-b/initrd.cpio
-    CONFIG_TXT
+    # Note that this test depends on the extensive boot configuration
+    # done at the suite prepare stage.
+
+    # Prepare for the switch to slot B. This is tested separately in another test.
+    test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = ""
 execute: |
     test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader CancelSwitch)" = ""
+
+    # The inactive slot configuration files were removed.
     test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt
+
+    # The active slot configuration files are pristine.
     test -e "$SYSOTA_BOOT_DIR"/config.txt
-    MATCH 'slot-a/' "$SYSOTA_BOOT_DIR"/config.txt
-restore: |
-    test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txe
-    rm -f "$SYSOTA_BOOT_DIR"/tryboot.txt
+    MATCH 'os_prefix=slot-a/' "$SYSOTA_BOOT_DIR"/config.txt
+
+    # The slot-specific boot directories originally prepared by the pre-install
+    # handler are pristine.
+    check-slot-a-b-pristine.sh
diff --git a/boot/piboot/tests/commit-switch/task.yaml b/boot/piboot/tests/commit-switch/task.yaml
index c3accdfc117110bf8242decf4e2c4592da4b6dc0..ab5834439ac41e76beebf15ef9d9314fa55beee6 100644
--- a/boot/piboot/tests/commit-switch/task.yaml
+++ b/boot/piboot/tests/commit-switch/task.yaml
@@ -6,13 +6,21 @@ details: |
     The CommitSwitch function renames the tryboot.txt file to config.txt, thus
     cementing the single-boot configuration for subsequent reboots.
 prepare: |
-    cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/tryboot.txt
-    kernel=slot-b/kernel
-    cmdline=slot-b/cmdline.txt
-    initramfs slot-b/initrd.cpio
-    CONFIG_TXT
+    # Note that this test depends on the extensive boot configuration
+    # done at the suite prepare stage.
+
+    # Prepare for the switch to slot B. This is tested separately in another test.
+    test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = ""
 execute: |
     test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader CommitSwitch)" = ""
+
+    # The tryboot.txt file is now gone.
     test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt
+
+    # The config.txt now points to slot-b and a dedicated cmdline file.
     test -e "$SYSOTA_BOOT_DIR"/config.txt
-    MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/config.txt
+    MATCH 'os_prefix=slot-b/' "$SYSOTA_BOOT_DIR"/config.txt
+
+    # The slot-specific boot directories originally prepared by the pre-install
+    # handler are pristine.
+    check-slot-a-b-pristine.sh
diff --git a/boot/piboot/tests/smoke/task.yaml b/boot/piboot/tests/smoke/task.yaml
deleted file mode 100644
index f37c9a1251a7b855ee2172735885cee43f808761..0000000000000000000000000000000000000000
--- a/boot/piboot/tests/smoke/task.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-# SPDX-FileCopyrightText: Huawei Inc.
-
-summary: Smoke tests for the boot protocol for the Raspberry Pi
-execute: |
-    # Given the initial state, we see the A and B slots as active and inactive respectively
-    test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader QueryActive)" = 's "A"'
-    test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader QueryInactive)" = 's "B"'
-
-    # The TrySwitch function creates a tryboot.txt file, replacing slot A with slot B without affecting config.txt
-    test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt
-    test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = ""
-    MATCH 'slot-a/' "$SYSOTA_BOOT_DIR"/config.txt
-    MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/tryboot.txt
-
-    # The Reboot function invoked with the try-boot flag reboots the system,
-    # passing special argument picked up by the Raspberry Pi kernel.
-    test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader Reboot u 1)" = ""
-    test -f "$MOCK_REBOOT_LOG"
-    MATCH 'reboot 0\\ tryboot' "$MOCK_REBOOT_LOG"
-
-    # The CommitSwitch function replaces config.txt with tryboot.txt
-    test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader CommitSwitch)" = ""
-    test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt
-    test -e "$SYSOTA_BOOT_DIR"/config.txt
-    MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/config.txt
-restore: |
-    rm -f "$SYSOTA_BOOT_DIR"/tryboot.txt
diff --git a/boot/piboot/tests/try-switch/task.yaml b/boot/piboot/tests/try-switch/task.yaml
index f627551259176fb8c4e4866b9cdeb6a0c2e5d28b..897e15a73e3010cb3b13f3050f7b831df89427ad 100644
--- a/boot/piboot/tests/try-switch/task.yaml
+++ b/boot/piboot/tests/try-switch/task.yaml
@@ -6,10 +6,24 @@ details: |
     The TrySwitch function creates a tryboot.txt file, replacing slot A with
     slot B without affecting config.txt. The contents of tryboot.txt is based on
     the contents of config.txt.
-execute: |
+prepare: |
+    # Note that this test depends on the extensive boot configuration
+    # done at the suite prepare stage.
+
+    # There is no configuration for the inactive slot prior to our call.
     test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt
+execute: |
     test "$(busctl call dev.ostc.sysota1 /dev/ostc/sysota1/Service dev.ostc.sysota1.BootLoader TrySwitch s "B")" = ""
-    MATCH 'slot-a/' "$SYSOTA_BOOT_DIR"/config.txt
-    MATCH 'slot-b/' "$SYSOTA_BOOT_DIR"/tryboot.txt
-restore: |
-    rm -f "$SYSOTA_BOOT_DIR"/tryboot.txt
+    MATCH 'os_prefix=slot-a/' "$SYSOTA_BOOT_DIR"/config.txt
+
+    # The active slot configuration files are pristine.
+    test -e "$SYSOTA_BOOT_DIR"/config.txt
+    MATCH 'os_prefix=slot-a/' "$SYSOTA_BOOT_DIR"/config.txt
+
+    # The inactive slot files are configured properly.
+    test -f "$SYSOTA_BOOT_DIR/tryboot.txt"
+    MATCH 'os_prefix=slot-b/' "$SYSOTA_BOOT_DIR"/tryboot.txt
+
+    # The slot-specific boot directories originally prepared by the pre-install
+    # handler are pristine.
+    check-slot-a-b-pristine.sh
diff --git a/cspell.json b/cspell.json
index 9a67566b0de9901e1c69d80e4aec186d29bf8749..5d5f0629a42a37671698fbd6192741f5d3351fb6 100644
--- a/cspell.json
+++ b/cspell.json
@@ -53,6 +53,7 @@
         "fdatasync", // file data synchronization system call
         "FDCWD", // file descriptor of current working directory
         "findstring", // find string, a make function
+        "followkernel", // option specific to Raspberry Pi config.txt initramfs parameter
         "freedesktop", // free desktop organization
         "froz", // one of meta variable names
         "frozbonicator", // one of meta variable names
diff --git a/spread.yaml b/spread.yaml
index eff522f4649dd0c426234798f2184770e2a20f75..044a07589fd3ebebcd7b2045f772f29f557e6089 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -103,6 +103,7 @@ suites:
             # Note: the variable is not special to sysotad, it is only used in tests.
             SYSOTA_BOOT_DIR: /var/tmp/sysota-fake/boot
             MOCK_REBOOT_LOG: /var/tmp/reboot.log
+            PATH: $PATH:$SPREAD_PATH/tests/bin:$SPREAD_PATH/boot/piboot/tests/bin
         prepare: |
             # Prepare sysotad.conf file that forces the use of pi-boot and sets
             # the location of the boot directory to SYSOTA_BOOT_DIR. Note that
@@ -125,28 +126,40 @@ suites:
             # In prepare-each we restore this state from a helper tarball.
 
             mkdir -p "$SYSOTA_BOOT_DIR"
-            mkdir -p "$SYSOTA_BOOT_DIR/slot-a/"
+            mkdir -p "$SYSOTA_BOOT_DIR"/slot-a
+            mkdir -p "$SYSOTA_BOOT_DIR"/slot-b
 
-            cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/slot-a/config.txt
-            # The fake config.txt as plausibly copied by pre-install handler and as
-            # present in the system image.
+            # Data for the slot A (presumed active).
+
+            cat <<SLOT_A_CONFIG_TXT >"$SYSOTA_BOOT_DIR"/slot-a/config.txt
             kernel=kernel.img
-            cmdline=cmdline.txt
-            initramfs initrd.cpio
-            CONFIG_TXT
+            SLOT_A_CONFIG_TXT
+
+            cat <<SLOT_A_CMDLINE_TXT >"$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt
+            cmdline-text root=$TEST_RAUC_FAKE_SYSTEM_DIR/slot-a sysota.slot=A
+            SLOT_A_CMDLINE_TXT
 
-            cat <<CMDLINE_TXT >"$SYSOTA_BOOT_DIR"/slot-a/cmdline.txt
-            fake-unused-cmdline
-            CMDLINE_TXT
+            # Data for the slot B.
+
+            cat <<SLOT_B_CONFIG_TXT >"$SYSOTA_BOOT_DIR"/slot-b/config.txt
+            kernel=kernel.img
+            SLOT_B_CONFIG_TXT
+
+            cat <<SLOT_B_CMDLINE_TXT >"$SYSOTA_BOOT_DIR"/slot-b/cmdline.txt
+            cmdline-text root=$TEST_RAUC_FAKE_SYSTEM_DIR/slot-b sysota.slot=B
+            SLOT_B_CMDLINE_TXT
+
+            # Boot configuration for the active slot. This configuration differs
+            # from that of slot-a/config.txt by the presence of os_prefix=.
 
-            # The config.txt file as derived from slot-a/config.txt with slot-a/
-            # prefix added to kernel=, cmdline= and initramfs parameters.
             cat <<CONFIG_TXT >"$SYSOTA_BOOT_DIR"/config.txt
-            kernel=slot-a/kernel.img
-            cmdline=slot-a/cmdline.txt
-            initramfs slot-a/initrd.cpio
+            kernel=kernel.img
+            os_prefix=slot-a/
             CONFIG_TXT
 
+            # Note that the vanilla data does not define tryboot.txt. This is
+            # done in individual tests which require it.
+
             tar -zcf /var/tmp/vanilla-boot.tar.gz -C "$SYSOTA_BOOT_DIR" .
 
             # Restart sysotad.service at least once to make sure it load the new
@@ -204,7 +217,6 @@ suites:
 
         restore-each: |
             rm -f "$MOCK_REBOOT_LOG"
-            test ! -e "$SYSOTA_BOOT_DIR"/tryboot.txt
 
             # Restore bootloader configuration to vanilla state.
             rm -rf "$SYSOTA_BOOT_DIR"