[PATCH v3 10/16] add support for fully rootless builds

'Felix Moessbauer' via isar-users
2026-04-07T14:23:41Z

Currently isar requires passwordless sudo and an environment
where mounting file systems is possible. This has proven problematic
for security reasons, both when running in a privileged container or
locally.

To solve this, we implement fully rootless builds that rely on the
unshare syscall which allows us to avoid sudo and instead operate in
temporary kernel namespaces as a user that is just privileged within
that namespace. This comes with some challenges regarding the handling
of mounts (they are cleared when leaving the namespace), as well as
cross namespace deployments (the outer user might not be able to access
the inner data). For that, we rework the handling of mounts and artifact
passing to make it compatible with both chroot modes (schroot and
unshare).

Signed-off-by: Felix Moessbauer <felix.moessbauer@siemens.com>
---
Kconfig | 2 +-
RECIPE-API-CHANGELOG.md | 21 +++++
doc/user_manual.md | 2 +
kas/isar.yaml | 2 +-
meta/classes-global/base.bbclass | 86 ++++++++++++++++++-
meta/classes-recipe/deb-dl-dir.bbclass | 9 +-
meta/classes-recipe/dpkg-base.bbclass | 22 ++++-
meta/classes-recipe/dpkg.bbclass | 17 ++--
.../image-locales-extension.bbclass | 9 +-
.../image-tools-extension.bbclass | 84 ++++++++++++++++++
meta/classes-recipe/image.bbclass | 7 +-
.../imagetypes_container.bbclass | 4 +-
meta/classes-recipe/imagetypes_wic.bbclass | 6 +-
meta/classes-recipe/rootfs.bbclass | 52 ++++++++---
meta/classes-recipe/sbuild.bbclass | 24 +++++-
meta/classes-recipe/sdk.bbclass | 10 ++-
meta/conf/bitbake.conf | 7 +-
.../isar-mmdebstrap/isar-mmdebstrap.inc | 18 ++--
.../sbuild-chroot/sbuild-chroot.inc | 24 +++++-
19 files changed, 362 insertions(+), 44 deletions(-)

diff --git a/Kconfig b/Kconfig
index 86a4aac3..66dd4112 100644
--- a/Kconfig
+++ b/Kconfig
@@ -14,7 +14,7 @@ config KAS_INCLUDE_MAIN

config KAS_BUILD_SYSTEM
string
- default "isar"
+ default "isar-rootless"

source "kas/machine/Kconfig"
source "kas/distro/Kconfig"
diff --git a/RECIPE-API-CHANGELOG.md b/RECIPE-API-CHANGELOG.md
index d0aa6e1a..27d14dc4 100644
--- a/RECIPE-API-CHANGELOG.md
+++ b/RECIPE-API-CHANGELOG.md
@@ -1077,3 +1077,24 @@ specifies the rootfs path.
Using these helpers instead of direct `sudo` invocations centralizes platform-specific
privileged execution logic in `base.bbclass`. Direct use of `sudo` is discouraged
in downstream layers.
+
+### Rootless isar execution
+
+Isar is able to run without the need for `sudo` in an environment that
+allows unprivileged users to unshare the kernels `user namespace`. Further,
+a sufficiently large set of sub ids needs to be configured in `/etc/subuid` / `etc/subgid`.
+This range should be `> 65536`, but smaller ranges might work as well, depending on the
+ids used in the rootfs.
+
+A simple check if rootless is supported can be done by running:
+
+```bash
+mmdebstrap --unshare-helper /bin/echo "rootless supported" || echo "rootless not supported"
+```
+
+To enable rootless builds, set the bitbake variable `ISAR_ROOTLESS = "1"`.
+This internally switches the chroot mode from `schroot` to `unshare`.
+
+When using kas, the `build_system` needs to be set to `isar-rootless`, but the final
+interfaces still need to be clarified. Further, kas patches are needed (for details,
+check the kas mailing list).
diff --git a/doc/user_manual.md b/doc/user_manual.md
index 69e8dfef..26041f9a 100644
--- a/doc/user_manual.md
+++ b/doc/user_manual.md
@@ -74,6 +74,7 @@ Building `debian-trixie` requires host system >= bookworm.
Install the following packages:
```
apt install \
+ acl \
binfmt-support \
bubblewrap \
bzip2 \
@@ -88,6 +89,7 @@ apt install \
qemu-user-static \
reprepro \
sudo \
+ uidmap \
unzip \
xz-utils \
git-buildpackage \
diff --git a/kas/isar.yaml b/kas/isar.yaml
index 16ce8b42..3cfc4f96 100644
--- a/kas/isar.yaml
+++ b/kas/isar.yaml
@@ -4,7 +4,7 @@
header:
version: 14

-build_system: isar
+build_system: isar-rootless

repos:
isar:
diff --git a/meta/classes-global/base.bbclass b/meta/classes-global/base.bbclass
index 90e4525e..7167cbb1 100644
--- a/meta/classes-global/base.bbclass
+++ b/meta/classes-global/base.bbclass
@@ -141,7 +141,9 @@ root_cleandirs() {
die "Could not remove $i, because subdir is mounted"
done
for i in $ROOT_CLEANDIRS_DIRS; do
- run_privileged rm -rf --one-file-system "$TMPDIR$i"
+ [ -d "$TMPDIR$i" ] || continue
+ find "$TMPDIR$i" \( ! -user "$(whoami)" -type d -prune \) -exec ${RUN_PRIVILEGED_CMD} rm -rf --one-file-system {} \;
+ rm -rf --one-file-system "$TMPDIR$i"
mkdir -p "$TMPDIR$i"
done
}
@@ -380,7 +382,28 @@ def deb_list_beautify(d, varname):
# shall be used outside of this class.

def insert_isar_mounts(d, rootfs, mounts):
+ """
+ In unshare mode, all mounts must be created after unsharing the
+ mount namespace. As needs to happen within the unshared session,
+ we implement it as a code generator. Note, that the random and urandom
+ mounts are needed for DDI images.
+ """
lines = []
+ to_touch = ['/dev/null', '/dev/random', '/dev/urandom']
+ to_mkdir = ['/dev/pts', '/dev/shm']
+ if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+ lines.append('touch ' + ' '.join(['{}/{}'.format(rootfs, f) for f in to_touch]))
+ lines.append('mkdir -p ' + ' '.join(['{}/{}'.format(rootfs, f) for f in to_mkdir]))
+ lines.append('mount -o bind,private,mode=666 /dev/null {}/dev/null'.format(rootfs))
+ lines.append('mount -t devpts -o noexec,nosuid,uid=5,mode=620,ptmxmode=666 none {}/dev/pts'.format(rootfs))
+ lines.append('( cd {}/dev; ln -sf pts/ptmx . )'.format(rootfs))
+ lines.append('mount -t tmpfs none {}/dev/shm'.format(rootfs))
+ lines.append('mount -o bind /dev/random {}/dev/random'.format(rootfs))
+ lines.append('mount -o bind /dev/urandom {}/dev/urandom'.format(rootfs))
+ lines.append('mount -t proc none {}/proc'.format(rootfs))
+ # we do not unshare the network namespace, so we cannot create a sysfs, hence bind-mount
+ lines.append('mount -o rbind /sys {}/sys'.format(rootfs))
+
for m in mounts.split():
host, inner = m.split(':') if ':' in m else (m, m)
inner_full = os.path.join(rootfs, inner[1:])
@@ -389,7 +412,18 @@ def insert_isar_mounts(d, rootfs, mounts):
return '\n'.join(lines)

def insert_isar_umounts(d, rootfs, mounts):
+ """
+ In unshare mount we don't unmount the system mounts but just
+ remove the mountpoints.
+ """
lines = []
+ to_unlink = ['/dev/null', '/dev/random', '/dev/urandom', '/dev/ptmx']
+ to_rmdir = ['/dev/pts', '/dev/shm']
+ if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+ lines.append('rm -f ' + ' '.join(['{}/{}'.format(rootfs, f) for f in to_unlink]))
+ for d in ['{}/{}'.format(rootfs, _d) for _d in to_rmdir]:
+ lines.append('[ -d {} ] && rmdir {}'.format(d, d))
+
for m in mounts.split():
host, inner = m.split(':') if ':' in m else (m, m)
mp = '{}/{}'.format(rootfs, inner)
@@ -397,11 +431,52 @@ def insert_isar_umounts(d, rootfs, mounts):
lines.append('[ -d {} ] && rmdir --ignore-fail-on-non-empty {}'.format(mp, mp))
return '\n'.join(lines)

+def get_subid_range(idmap, d):
+ import getpass
+ with open(idmap, 'r') as f:
+ entries = f.readlines()
+ for e in entries:
+ user, base, cnt = e.split(':')
+ if user == os.getuid() or user == getpass.getuser():
+ return int(base), int(cnt)
+ bb.error("No sub-id range specified in %s" % idmap)
+
def run_privileged_cmd(d):
- cmd = 'sudo -E'
+ """
+ In unshare mode we need to map the rootfs uid/gid range into the
+ subuid/subgid range of the parent namespace. As we usually only
+ get 65534 ids, we cannot map the whole range, as two ids are already
+ used by the calling environment (root and builder user). Hence, map
+ as much as we can but also map the highest id (nobody / nogroup) as
+ these are used within the rootfs. It would be easier to use
+ mmdebstrap --unshare-helper as command (which is also internally used
+ by sbuild), but this only maps linear ranges, hence it cannot map the
+ nobody / nogroup on the default subid range. By that, we have to avoid
+ the nobody / nogroup when building packages in this case.
+ """
+ if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+ nobody_id = 65534
+ uid_base, uid_cnt = get_subid_range('/etc/subuid', d)
+ nobody_subid = uid_base + uid_cnt - 1
+ gid_base, gid_cnt = get_subid_range('/etc/subgid', d)
+ nogroup_subid = gid_base + gid_cnt - 1
+ cmd = 'unshare --mount --pid --uts --ipc --user' \
+ ' --kill-child' \
+ ' --setuid 0 --setgid 0 --fork' \
+ f' --map-users 1:{uid_base+1}:{uid_cnt-2}' \
+ f' --map-groups 1:{gid_base+1}:{gid_cnt-2}'
+ if uid_cnt < nobody_id:
+ cmd += f' --map-users {nobody_id}:{nobody_subid}:1'
+ if gid_cnt < nobody_id:
+ cmd += f' --map-groups {nobody_id}:{nogroup_subid}:1'
+ cmd += " --map-root-user"
+ else:
+ cmd = 'sudo -E'
bb.debug(1, "privileged cmd: %s" % cmd)
return cmd

+UNSHARE_SUBUID_BASE := "${@get_subid_range('/etc/subuid', d)[0] if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else '0'}"
+# store in variable to only compute once and make available to fetcher
RUN_PRIVILEGED_CMD := "${@run_privileged_cmd(d)}"

run_privileged() {
@@ -415,5 +490,10 @@ run_privileged_heredoc() {
run_in_chroot() {
rootfs="$1"
shift
- ${RUN_PRIVILEGED_CMD} chroot "$rootfs" "$@"
+
+ rootfs=$rootfs run_privileged_heredoc <<'EORIC' "$@"
+ set -e
+ ${@insert_isar_mounts(d, '$rootfs', '')}
+ chroot "$rootfs" "$@"
+EORIC
}
diff --git a/meta/classes-recipe/deb-dl-dir.bbclass b/meta/classes-recipe/deb-dl-dir.bbclass
index 04fd6414..0e268f06 100644
--- a/meta/classes-recipe/deb-dl-dir.bbclass
+++ b/meta/classes-recipe/deb-dl-dir.bbclass
@@ -123,8 +123,13 @@ deb_dl_dir_import() {

# let our unprivileged user place downloaded packages in /var/cache/apt/archives/
run_privileged_heredoc << ' EOSUDO'
- mkdir -p "${rootfs}"/var/cache/apt/archives/partial/
- chown -R ${uid}:${gid} "${rootfs}"/var/cache/apt/archives/
+ if [ "${ISAR_CHROOT_MODE}" = "unshare" ]; then
+ mkdir -p "${rootfs}"/var/cache/apt/archives
+ chmod 777 "${rootfs}"/var/cache/apt/archives
+ else
+ mkdir -p "${rootfs}"/var/cache/apt/archives/partial/
+ chown -R ${uid}:${gid} "${rootfs}"/var/cache/apt/archives/
+ fi
EOSUDO

# nothing to copy if download directory does not exist just yet
diff --git a/meta/classes-recipe/dpkg-base.bbclass b/meta/classes-recipe/dpkg-base.bbclass
index e8721c79..a0d4fd05 100644
--- a/meta/classes-recipe/dpkg-base.bbclass
+++ b/meta/classes-recipe/dpkg-base.bbclass
@@ -168,12 +168,30 @@ dpkg_schroot_create_configs() {
EOSUDO
}

+dpkg_chroot_prepare() {
+ if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+ dpkg_schroot_create_configs
+ fi
+}
+
+dpkg_chroot_finalize() {
+ if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+ schroot_delete_configs
+ fi
+}
+
+dpkg_prepare_unshare_ccache() {
+ mkdir -p "${CCACHE_DIR}"
+ # sbuild id from https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1110942
+ setfacl -m u:${UNSHARE_SUBUID_BASE}:rwX -m u:${@int(d.getVar('UNSHARE_SUBUID_BASE')) + 999}:rwx "${CCACHE_DIR}"
+}
+
python do_dpkg_build() {
- bb.build.exec_func('dpkg_schroot_create_configs', d)
+ bb.build.exec_func('dpkg_chroot_prepare', d)
try:
bb.build.exec_func("dpkg_runbuild", d)
finally:
- bb.build.exec_func('schroot_delete_configs', d)
+ bb.build.exec_func('dpkg_chroot_finalize', d)
}
do_dpkg_build[network] = "${TASK_USE_NETWORK_AND_SUDO}"

diff --git a/meta/classes-recipe/dpkg.bbclass b/meta/classes-recipe/dpkg.bbclass
index dcdef487..57fe042b 100644
--- a/meta/classes-recipe/dpkg.bbclass
+++ b/meta/classes-recipe/dpkg.bbclass
@@ -85,7 +85,10 @@ dpkg_runbuild() {
ext_deb_dir="${ext_root}${deb_dir}"

if [ ${USE_CCACHE} -eq 1 ]; then
- schroot_configure_ccache
+ ${ISAR_CHROOT_MODE}_configure_ccache
+ fi
+ if [ "${ISAR_CHROOT_MODE}" = "unshare" ]; then
+ sbuild_add_unshare_mounts
fi

profiles="${@ isar_deb_build_profiles(d)}"
@@ -109,23 +112,27 @@ dpkg_runbuild() {

DSC_FILE=$(find ${WORKDIR} -maxdepth 1 -name "${DEBIAN_SOURCE}_*.dsc" -print)

- sbuild -A -n -c ${SBUILD_CHROOT} --chroot-mode=schroot \
+ sbuild -A -n -c ${SBUILD_CHROOT} \
+ --chroot-mode=${ISAR_CHROOT_MODE} \
--host=${PACKAGE_ARCH} --build=${BUILD_ARCH} ${profiles} \
--no-run-lintian --no-run-piuparts --no-run-autopkgtest --resolve-alternatives \
--bd-uninstallable-explainer=apt \
--no-apt-update --apt-distupgrade \
--chroot-setup-commands="echo \"Package: *\nPin: release n=${DEBDISTRONAME}\nPin-Priority: 1000\" > /etc/apt/preferences.d/isar-apt" \
- --chroot-setup-commands="echo \"APT::Get::allow-downgrades 1;\" > /etc/apt/apt.conf.d/50isar-apt" \
+ --chroot-setup-commands="echo \"APT::Get::allow-downgrades 1;${@'\nAPT::Sandbox::User root;' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}\" > /etc/apt/apt.conf.d/50isar-apt" \
--chroot-setup-commands="rm -f /var/log/dpkg.log" \
--chroot-setup-commands="mkdir -p ${deb_dir}" \
--chroot-setup-commands="find ${ext_deb_dir} -maxdepth 1 -name '*.deb' -exec ln -t ${deb_dir}/ -sf {} +" \
--chroot-setup-commands="apt-get update -o Dir::Etc::SourceList=\"sources.list.d/isar-apt.list\" -o Dir::Etc::SourceParts=\"-\" -o APT::Get::List-Cleanup=\"0\"" \
--finished-build-commands="rm -f ${deb_dir}/sbuild-build-depends-*-dummy_*.deb" \
--finished-build-commands="find ${deb_dir} -maxdepth 1 -type f -name '*.deb' -print -exec cp ${CP_FLAGS} -t ${ext_deb_dir}/ {} +" \
- --finished-build-commands="cp /var/log/dpkg.log ${ext_root}/dpkg_partial.log" \
+ ${@ '--finished-build-commands="cp /var/log/dpkg.log $ext_root/dpkg_partial.log"' if d.getVar('ISAR_CHROOT_MODE') == 'schroot' else '' } \
--build-path="" --build-dir=${WORKDIR} --dist="${DEBDISTRONAME}" ${DSC_FILE}

- sbuild_dpkg_log_export "${WORKDIR}/rootfs/dpkg_partial.log"
+ # TODO: port to unshare backend
+ if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+ sbuild_dpkg_log_export "${WORKDIR}/rootfs/dpkg_partial.log"
+ fi
deb_dl_dir_export "${WORKDIR}/rootfs" "${distro}"

# Cleanup apt artifacts
diff --git a/meta/classes-recipe/image-locales-extension.bbclass b/meta/classes-recipe/image-locales-extension.bbclass
index 029caec7..9bb43a8d 100644
--- a/meta/classes-recipe/image-locales-extension.bbclass
+++ b/meta/classes-recipe/image-locales-extension.bbclass
@@ -29,8 +29,12 @@ ROOTFS_INSTALL_COMMAND_BEFORE_EXPORT += "image_install_localepurge_download"
image_install_localepurge_download[weight] = "40"
image_install_localepurge_download[network] = "${TASK_USE_NETWORK_AND_SUDO}"
image_install_localepurge_download() {
- run_in_chroot '${ROOTFSDIR}' \
+ run_privileged_heredoc <<'EOF'
+ set -e
+ ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS') if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else '')}
+ chroot ${ROOTFSDIR} \
/usr/bin/apt-get ${ROOTFS_APT_ARGS} -oDebug::NoLocking=1 --download-only localepurge
+EOF
}

ROOTFS_INSTALL_COMMAND += "image_install_localepurge_install"
@@ -62,6 +66,9 @@ __EOF__
# Install configuration into image:
run_privileged_heredoc <<'EOSUDO'
set -e
+
+ ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), '')}
+
localepurge_state='i'
if chroot '${ROOTFSDIR}' dpkg -s localepurge 2>/dev/null >&2
then
diff --git a/meta/classes-recipe/image-tools-extension.bbclass b/meta/classes-recipe/image-tools-extension.bbclass
index 766f386d..cc046fdb 100644
--- a/meta/classes-recipe/image-tools-extension.bbclass
+++ b/meta/classes-recipe/image-tools-extension.bbclass
@@ -16,7 +16,14 @@ do_image_tools[depends] += " \
SCHROOT_MOUNTS = "${WORKDIR}:${PP_WORK} ${IMAGE_ROOTFS}:${PP_ROOTFS} ${DEPLOY_DIR_IMAGE}:${PP_DEPLOY}"
SCHROOT_MOUNTS += "${REPO_ISAR_DIR}/${DISTRO}:/isar-apt"

+# only used on unshare
+ROOTFS_IMAGETOOLS ?= "${WORKDIR}/rootfs-imgtools-${BB_CURRENTTASK}"
+
imager_run() {
+ imager_run_${ISAR_CHROOT_MODE} "$@"
+}
+
+imager_run_schroot() {
local_install="${@(d.getVar("INSTALL_%s" % d.getVar("BB_CURRENTTASK")) or '').strip()}"
local_bom="${@(d.getVar("BOM_%s" % d.getVar("BB_CURRENTTASK")) or '').strip()}"

@@ -103,3 +110,80 @@ generate_imager_sbom() {
--timestamp $TIMESTAMP ${SBOM_DEBSBOM_EXTRA_ARGS} \
< ${WORKDIR}/imager.manifest
}
+
+imager_run_unshare() {
+ exec 3<&0
+
+ # ignore everything before '--'. If the remaining list is empty,
+ # assume a here document is passed via stdin
+ while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --) shift 1; break ;;
+ *) shift 1 ;;
+ esac
+ done
+
+ if [ "$#" -eq 0 ]; then
+ set -- "$@" '/bin/bash' '-s'
+ fi
+
+ local_install="${@(d.getVar("INSTALL_%s" % d.getVar("BB_CURRENTTASK")) or '').strip()}"
+
+ run_privileged_heredoc <<'EOF'
+ set -e
+ mkdir -p ${ROOTFS_IMAGETOOLS}
+ tar -xf "${SBUILD_CHROOT}" -C "${ROOTFS_IMAGETOOLS}"
+ mkdir -p ${ROOTFS_IMAGETOOLS}/isar-apt
+ cp -rL /etc/resolv.conf "${ROOTFS_IMAGETOOLS}/etc"
+EOF
+
+ # setting up error handler
+ imager_cleanup() {
+ run_privileged rm -rf ${ROOTFS_IMAGETOOLS}
+ }
+ trap 'exit 1' INT HUP QUIT TERM ALRM USR1
+ trap 'imager_cleanup' EXIT
+
+ if [ -n "${local_install}" ]; then
+ echo "Installing imager deps: ${local_install}"
+
+ distro="${BASE_DISTRO}-${BASE_DISTRO_CODENAME}"
+ if [ ${ISAR_CROSS_COMPILE} -eq 1 ]; then
+ distro="${HOST_BASE_DISTRO}-${BASE_DISTRO_CODENAME}"
+ fi
+
+ E="${@ isar_export_proxies(d)}"
+ deb_dl_dir_import ${ROOTFS_IMAGETOOLS} ${distro}
+ ${SCRIPTSDIR}/lockrun.py -r -f "${REPO_ISAR_DIR}/isar.lock" -s <<'EOAPT'
+ local_install=$local_install ${@run_privileged_cmd(d)} /bin/bash -s <<'EOF'
+ set -e
+ ${@insert_isar_mounts(d, d.getVar('ROOTFS_IMAGETOOLS'), d.getVar('SCHROOT_MOUNTS'))}
+ chroot ${ROOTFS_IMAGETOOLS} apt-get update \
+ -o Dir::Etc::SourceList='sources.list.d/isar-apt.list' \
+ -o Dir::Etc::SourceParts='-' \
+ -o APT::Get::List-Cleanup='0'
+ chroot ${ROOTFS_IMAGETOOLS} apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y \
+ --allow-unauthenticated --allow-downgrades --download-only install \
+ $local_install
+EOF
+EOAPT
+
+ deb_dl_dir_export ${ROOTFS_IMAGETOOLS} ${distro}
+ local_install=$local_install run_privileged_heredoc <<'EOF'
+ set -e
+ ${@insert_isar_mounts(d, d.getVar('ROOTFS_IMAGETOOLS'), d.getVar('SCHROOT_MOUNTS'))}
+ chroot ${ROOTFS_IMAGETOOLS} apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y \
+ --allow-unauthenticated --allow-downgrades install \
+ $local_install
+EOF
+ fi
+
+ run_privileged_heredoc <<'EOF' "$@"
+ set -e
+ mkdir -p ${ROOTFS_IMAGETOOLS}/${SCRIPTSDIR}
+ ${@insert_isar_mounts(d, d.getVar('ROOTFS_IMAGETOOLS'), d.getVar('SCHROOT_MOUNTS'))}
+ chroot ${ROOTFS_IMAGETOOLS} "$@" <&3
+EOF
+
+ run_privileged rm -rf ${ROOTFS_IMAGETOOLS}
+}
diff --git a/meta/classes-recipe/image.bbclass b/meta/classes-recipe/image.bbclass
index 9fcdda48..0fa15a87 100644
--- a/meta/classes-recipe/image.bbclass
+++ b/meta/classes-recipe/image.bbclass
@@ -189,6 +189,7 @@ SUDO_CHROOT = "imager_run -d ${PP_ROOTFS} -u root --"
python() {
image_types = (d.getVar('IMAGE_FSTYPES') or '').split()
conversions = set(d.getVar('IMAGE_CONVERSIONS').split())
+ chroot_mode = d.getVar('ISAR_CHROOT_MODE')

basetypes = {}
typedeps = {}
@@ -264,7 +265,8 @@ python() {
if image_cmd:
localdata.setVar('type', bt)
cmds.append(localdata.expand(image_cmd))
- cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}'))
+ if chroot_mode == 'schroot':
+ cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}'))
else:
bb.fatal("No IMAGE_CMD for %s" % bt)
vardeps.add('IMAGE_CMD:' + bt_clean)
@@ -294,7 +296,8 @@ python() {
cmd = '\t' + localdata.getVar('CONVERSION_CMD:' + c)
if cmd not in cmds:
cmds.append(cmd)
- cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}.%s' % c))
+ if chroot_mode == 'schroot':
+ cmds.append(localdata.expand('\tsudo chown $(id -u):$(id -g) ${IMAGE_FILE_HOST}.%s' % c))
vardeps.add('CONVERSION_CMD:' + c)
for dep in (localdata.getVar('CONVERSION_DEPS:' + c) or '').split():
conversion_install.add(dep)
diff --git a/meta/classes-recipe/imagetypes_container.bbclass b/meta/classes-recipe/imagetypes_container.bbclass
index fb1d0cdf..a68438e9 100644
--- a/meta/classes-recipe/imagetypes_container.bbclass
+++ b/meta/classes-recipe/imagetypes_container.bbclass
@@ -68,7 +68,9 @@ do_containerize() {
run_privileged rm -rf "${oci_img_dir}_unpacked"

# no root needed anymore
- run_privileged chown --recursive $(id -u):$(id -g) "${oci_img_dir}"
+ if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+ run_privileged chown --recursive $(id -u):$(id -g) "${oci_img_dir}"
+ fi
}

convert_container() {
diff --git a/meta/classes-recipe/imagetypes_wic.bbclass b/meta/classes-recipe/imagetypes_wic.bbclass
index 8b048dc7..3e261622 100644
--- a/meta/classes-recipe/imagetypes_wic.bbclass
+++ b/meta/classes-recipe/imagetypes_wic.bbclass
@@ -193,8 +193,10 @@ generate_wic_image() {
fi
EOIMAGER

- run_privileged chown -R $(stat -c "%U" ${LAYERDIR_core}) ${LAYERDIR_core} ${LAYERDIR_isar} ${SCRIPTSDIR} || true
- run_privileged chown -R $(id -u):$(id -g) "${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}.wic"*
+ if [ "${ISAR_CHROOT_MODE}" = "schroot" ]; then
+ run_privileged chown -R $(stat -c "%U" ${LAYERDIR_core}) ${LAYERDIR_core} ${LAYERDIR_isar} ${SCRIPTSDIR} || true
+ run_privileged chown -R $(id -u):$(id -g) "${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}.wic"*
+ fi
rm -rf ${IMAGE_ROOTFS}/../pseudo

cat ${DEPLOY_DIR_IMAGE}/${IMAGE_FULLNAME}.manifest \
diff --git a/meta/classes-recipe/rootfs.bbclass b/meta/classes-recipe/rootfs.bbclass
index 7352a87c..f31964db 100644
--- a/meta/classes-recipe/rootfs.bbclass
+++ b/meta/classes-recipe/rootfs.bbclass
@@ -145,7 +145,12 @@ rootfs_cmd() {
}

rootfs_do_mounts[weight] = "3"
-rootfs_do_mounts() {
+python rootfs_do_mounts() {
+ if d.getVar('ISAR_CHROOT_MODE') == 'schroot':
+ bb.build.exec_func('rootfs_do_mounts_priv', d)
+}
+
+rootfs_do_mounts_priv() {
run_privileged_heredoc <<'EOSUDO'
set -e
mountpoint -q '${ROOTFSDIR}/dev' || \
@@ -168,7 +173,13 @@ rootfs_do_mounts() {
EOSUDO
}

-rootfs_do_umounts() {
+python rootfs_do_umounts() {
+ # unconditionally run the unmount code as this ignores missing
+ # mountpoints but also does the cleanup of the directories
+ bb.build.exec_func('rootfs_do_umounts_priv', d)
+}
+
+rootfs_do_umounts_priv() {
run_privileged_heredoc <<'EOSUDO'
set -e

@@ -215,7 +226,11 @@ ROOTFS_EXTRA_IMPORTED := "${@rootfs_extra_import(d)}"

rootfs_prepare[weight] = "25"
rootfs_prepare(){
- run_privileged tar -xf "${BOOTSTRAP_SRC}" -C "${ROOTFSDIR}" --exclude="./dev/console"
+ rm -rf ${ROOTFSDIR}
+ run_privileged_heredoc << 'EOF'
+ mkdir -p ${ROOTFSDIR}
+ tar -xf "${BOOTSTRAP_SRC}" -C "${ROOTFSDIR}" --exclude="./dev/console"
+EOF

# setup chroot
run_privileged "${ROOTFSDIR}/chroot-setup.sh" "setup" "${ROOTFSDIR}"
@@ -285,10 +300,14 @@ rootfs_install_pkgs_update[weight] = "5"
rootfs_install_pkgs_update[isar-apt-lock] = "acquire-before"
rootfs_install_pkgs_update[network] = "${TASK_USE_NETWORK_AND_SUDO}"
rootfs_install_pkgs_update() {
- run_in_chroot '${ROOTFSDIR}' /usr/bin/apt-get update \
- -o Dir::Etc::SourceList="sources.list.d/isar-apt.list" \
- -o Dir::Etc::SourceParts="-" \
- -o APT::Get::List-Cleanup="0"
+ run_privileged_heredoc <<'EOF'
+ set -e
+ ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+ chroot '${ROOTFSDIR}' /usr/bin/apt-get update \
+ -o Dir::Etc::SourceList="sources.list.d/isar-apt.list" \
+ -o Dir::Etc::SourceParts="-" \
+ -o APT::Get::List-Cleanup="0"
+EOF
}

ROOTFS_INSTALL_COMMAND += "rootfs_install_resolvconf"
@@ -316,9 +335,12 @@ rootfs_install_pkgs_download[isar-apt-lock] = "release-after"
rootfs_install_pkgs_download[network] = "${TASK_USE_NETWORK}"
rootfs_install_pkgs_download() {
# download packages using apt in a non-privileged namespace
- rootfs_cmd --bind "${ROOTFSDIR}/var/cache/apt/archives" /var/cache/apt/archives \
- ${ROOTFSDIR} \
- -- /usr/bin/apt-get ${ROOTFS_APT_ARGS} -oDebug::NoLocking=1 --download-only ${ROOTFS_PACKAGES}
+ run_privileged_heredoc <<'EOF'
+ set -e
+ ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+ chroot ${ROOTFSDIR} \
+ /usr/bin/apt-get ${ROOTFS_APT_ARGS} -oDebug::NoLocking=1 --download-only ${ROOTFS_PACKAGES}
+EOF
}

ROOTFS_INSTALL_COMMAND_BEFORE_EXPORT ??= ""
@@ -345,8 +367,12 @@ rootfs_install_pkgs_install[weight] = "8000"
rootfs_install_pkgs_install[progress] = "custom:rootfs_progress.PkgsInstallProgressHandler"
rootfs_install_pkgs_install[network] = "${TASK_USE_SUDO}"
rootfs_install_pkgs_install() {
- run_in_chroot "${ROOTFSDIR}" \
+ run_privileged_heredoc <<'EOF'
+ set -e
+ ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+ chroot "${ROOTFSDIR}" \
/usr/bin/apt-get ${ROOTFS_APT_ARGS} ${ROOTFS_PACKAGES}
+EOF
}

ROOTFS_INSTALL_COMMAND += "rootfs_restore_initrd_tooling"
@@ -653,8 +679,10 @@ rootfs_install_sstate_finalize() {
# - after building the rootfs, the tar won't be there, but we also don't need to unpack
# - after restoring from cache, there will be a tar which we unpack and then delete
if [ -f rootfs.tar ]; then
+ run_privileged_heredoc <<'EOF'
mkdir -p ${ROOTFSDIR}
- run_privileged tar -C ${ROOTFSDIR} -xp ${SSTATE_TAR_ATTR_FLAGS} < rootfs.tar
+ tar -C ${ROOTFSDIR} -xp ${SSTATE_TAR_ATTR_FLAGS} -f rootfs.tar
+EOF
rm rootfs.tar
fi
}
diff --git a/meta/classes-recipe/sbuild.bbclass b/meta/classes-recipe/sbuild.bbclass
index d9ccce7f..8ca66138 100644
--- a/meta/classes-recipe/sbuild.bbclass
+++ b/meta/classes-recipe/sbuild.bbclass
@@ -7,7 +7,8 @@ SCHROOT_MOUNTS ?= ""

inherit crossvars

-SBUILD_CHROOT ?= "${DEBDISTRONAME}-${SCHROOT_USER}-${ISAR_BUILD_UUID}-${@os.getpid()}"
+SBUILD_CHROOT:unshare ?= "${SCHROOT_DIR}.tar.zst"
+SBUILD_CHROOT:schroot ?= "${DEBDISTRONAME}-${SCHROOT_USER}-${ISAR_BUILD_UUID}-${@os.getpid()}"

SBUILD_CONF_DIR ?= "${SCHROOT_CONF}/${SBUILD_CHROOT}"
SCHROOT_CONF_FILE ?= "${SCHROOT_CONF}/chroot.d/${SBUILD_CHROOT}"
@@ -144,6 +145,13 @@ END
EOSUDO
}

+unshare_configure_ccache() {
+ # ccache must be below /build for file permissions to work properly
+ cat <<'EOF' >> ${SBUILD_CONFIG}
+$path = "/usr/lib/ccache:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games";
+EOF
+}
+
sbuild_dpkg_log_export() {
export dpkg_partial_log="${1}"

@@ -152,3 +160,17 @@ sbuild_dpkg_log_export() {
cat ${dpkg_partial_log} >> ${SCHROOT_DIR}/tmp/dpkg_common.log
) 9>"${SCHROOT_DIR}/tmp/dpkg_common.log.lock"
}
+
+# additional mounts managed by sbuild
+sbuild_add_unshare_mounts() {
+ dpkg_prepare_unshare_ccache
+
+ cat <<'EOF' >> ${SBUILD_CONFIG}
+$unshare_bind_mounts = [
+ { directory => '${WORKDIR}/rootfs', mountpoint => '${PP}/rootfs' },
+ { directory => '${WORKDIR}/isar-apt/${DISTRO}-${DISTRO_ARCH}/apt/${DISTRO}', mountpoint => '/isar-apt' },
+ { directory => '${REPO_BASE_DIR}', mountpoint => '/base-apt' },
+ { directory => "${CCACHE_DIR}", mountpoint => "/ccache" }
+];
+EOF
+}
diff --git a/meta/classes-recipe/sdk.bbclass b/meta/classes-recipe/sdk.bbclass
index 16165792..7a8d5ff4 100644
--- a/meta/classes-recipe/sdk.bbclass
+++ b/meta/classes-recipe/sdk.bbclass
@@ -74,13 +74,17 @@ rootfs_configure_isar_apt_dir() {

ROOTFS_POSTPROCESS_COMMAND:prepend:class-sdk = "sdkchroot_configscript "
sdkchroot_configscript () {
- run_in_chroot ${ROOTFSDIR} /configscript.sh ${DISTRO_ARCH}
+ run_privileged_heredoc <<'EOF'
+ set -e
+ ${@insert_isar_mounts(d, d.getVar('ROOTFSDIR'), d.getVar('ROOTFS_MOUNTS')) if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''}
+ cp -rL /etc/resolv.conf '${ROOTFSDIR}/etc'
+ chroot ${ROOTFSDIR} /configscript.sh ${DISTRO_ARCH}
+EOF
}

ROOTFS_POSTPROCESS_COMMAND:append:class-sdk = " sdkchroot_finalize"
sdkchroot_finalize() {
-
- rootfs_do_umounts
+ rootfs_do_umounts_priv

# Remove setup scripts
run_privileged rm -f ${ROOTFSDIR}/chroot-setup.sh ${ROOTFSDIR}/configscript.sh
diff --git a/meta/conf/bitbake.conf b/meta/conf/bitbake.conf
index 5f339d40..4d3fd62e 100644
--- a/meta/conf/bitbake.conf
+++ b/meta/conf/bitbake.conf
@@ -73,7 +73,7 @@ KERNEL_FILE:arm64 ?= "vmlinux"

MACHINEOVERRIDES ?= "${MACHINE}"
DISTROOVERRIDES ?= "${DISTRO}"
-OVERRIDES = "${PACKAGE_ARCH}:${MACHINEOVERRIDES}:${DISTROOVERRIDES}:${BASE_DISTRO_CODENAME}:forcevariable"
+OVERRIDES = "${PACKAGE_ARCH}:${MACHINEOVERRIDES}:${DISTROOVERRIDES}:${BASE_DISTRO_CODENAME}:${ISAR_CHROOT_MODE}:forcevariable"
FILESOVERRIDES = "${PACKAGE_ARCH}:${MACHINE}"

# Setting default QEMU_ARCH variables for different DISTRO_ARCH:
@@ -152,6 +152,10 @@ ISAR_APT_RETRIES ??= "${@'10' if bb.utils.to_boolean(d.getVar('ISAR_USE_APT_SNAP
ISAR_APT_DELAY_MAX ??= "${@'600' if bb.utils.to_boolean(d.getVar('ISAR_USE_APT_SNAPSHOT')) else ''}"
ISAR_APT_SNAPSHOT_TIMESTAMP ??= "${SOURCE_DATE_EPOCH}"

+# Rootless build execution
+ISAR_ROOTLESS ??= "0"
+ISAR_CHROOT_MODE ??= "${@'unshare' if bb.utils.to_boolean(d.getVar('ISAR_ROOTLESS')) else 'schroot'}"
+
# Default parallelism and resource usage for xz
XZ_MEMLIMIT ?= "50%"
XZ_THREADS ?= "${@oe.utils.cpu_count(at_least=2)}"
@@ -207,6 +211,7 @@ CCACHE_DEBUG ?= "0"
# Variables for tasks marking
# Long term TODO: get rid of sudo marked tasks
TASK_USE_NETWORK = "1"
+# nested namespacing requires this as well
TASK_USE_SUDO = "1"
TASK_USE_NETWORK_AND_SUDO = "1"

diff --git a/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc b/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc
index cf6c355c..4d102ed6 100644
--- a/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc
+++ b/meta/recipes-core/isar-mmdebstrap/isar-mmdebstrap.inc
@@ -161,6 +161,8 @@ do_bootstrap() {
line="[trusted=yes] ${line}"
fi
echo "deb-src ${line}" >> "${WORKDIR}/sources.list.d/base-apt.list"
+ echo > ${WORKDIR}/mmtmpdir
+ chmod 666 ${WORKDIR}/mmtmpdir

# no need to sync /var/cache/apt/archives if base-apt used
syncin='echo skip sync-in'
@@ -177,12 +179,14 @@ do_bootstrap() {
mkdir -p \$1/base-apt && \
mount -o bind,private '${REPO_BASE_DIR}' \$1/base-apt && \
chroot \$1 apt-get update -y \
- -o APT::Update::Error-Mode=any && \
+ -o APT::Update::Error-Mode=any \
+ ${@'-o APT::Sandbox::User=root' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''} && \
chroot \$1 apt-get install -y dpkg && \
umount \$1/base-apt && \
- umount \$1/$base_apt_tmp && rm ${WORKDIR}/mmtmpdir && \
- umount $base_apt_tmp && rm -rf --one-file-system $base_apt_tmp"
+ umount \$1/$base_apt_tmp && \
+ umount $base_apt_tmp && rmdir \$1/$base_apt_tmp"
else
+ # prepare dl_dir for access from both sides (local and rootfs)
deb_dl_dir_import "${WORKDIR}/dl_dir" "${BOOTSTRAP_BASE_DISTRO}-${BASE_DISTRO_CODENAME}"

bootstrap_list="${WORKDIR}/sources.list.d/bootstrap.list"
@@ -202,6 +206,7 @@ do_bootstrap() {
-o Dir::State="$1/var/lib/apt" \
-o Dir::Etc="$1/etc/apt" \
-o Dir::Cache="$1/var/cache/apt" \
+ ${@'-o APT::Sandbox::User=root' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''} \
-o Apt::Architecture="${BOOTSTRAP_DISTRO_ARCH}" \
${@get_apt_opts(d, '-o')}'
extra_essential="$extra_essential && $syncout"
@@ -225,7 +230,8 @@ do_bootstrap() {
mkdir -p ${DEBDIR}
touch ${DEB_DL_LOCK}

- run_privileged TMPDIR="${BOOTSTRAP_TMPDIR}" mmdebstrap $bootstrap_args \
+ ${@'' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else 'run_privileged'} \
+ TMPDIR="${BOOTSTRAP_TMPDIR}" mmdebstrap $bootstrap_args \
$arch_param \
--mode=unshare \
${MMHOOKS} \
@@ -244,6 +250,7 @@ do_bootstrap() {
--customize-hook='sed -i "/en_US.UTF-8 UTF-8/s/^#//g" "$1/etc/locale.gen"' \
--customize-hook='chroot "$1" /usr/sbin/locale-gen' \
--customize-hook='chroot "$1" /usr/bin/apt-get -y clean' \
+ ${@'--skip=output/dev' if d.getVar('ISAR_CHROOT_MODE') == 'unshare' else ''} \
--skip=cleanup/apt \
--skip=download/empty \
${MMOPTS} \
@@ -258,7 +265,8 @@ do_bootstrap() {

if [ "${ISAR_USE_CACHED_BASE_REPO}" != "1" ]; then
deb_dl_dir_export "${WORKDIR}/dl_dir" "${BOOTSTRAP_BASE_DISTRO}-${BASE_DISTRO_CODENAME}"
- run_privileged rm -rf --one-file-system "${WORKDIR}/dl_dir"
+ run_privileged find ${WORKDIR}/dl_dir -maxdepth 1 -mindepth 1 -exec rm -rf --one-file-system "{}" \;
+ rmdir ${WORKDIR}/dl_dir
fi
}
addtask bootstrap before do_build after do_generate_keyrings
diff --git a/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc b/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc
index aa62b324..054d7fc2 100644
--- a/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc
+++ b/meta/recipes-devtools/sbuild-chroot/sbuild-chroot.inc
@@ -66,8 +66,28 @@ ROOTFS_POSTPROCESS_COMMAND:remove = "rootfs_cleanup_base_apt"

DEPLOY_SCHROOT = "${@d.getVar('SCHROOT_' + d.getVar('SBUILD_VARIANT').upper() + '_DIR')}${SBUILD_SCHROOT_SUFFIX}"

-do_sbuildchroot_deploy[dirs] = "${DEPLOY_DIR}/schroot-${SBUILD_VARIANT}"
-do_sbuildchroot_deploy() {
+sbuildchroot_deploy_tree() {
ln -Tfsr "${ROOTFSDIR}" "${DEPLOY_SCHROOT}"
}
+sbuildchroot_deploy_tar() {
+ lopts="--one-file-system --exclude=var/cache/apt/archives --exclude=isar-apt"
+ # we cannot use pzstd, as this results in a different magic
+ # (zstd skippable frame) which is not detected by sbuild
+ # https://salsa.debian.org/debian/sbuild/-/blob/d975d388a98627a0d7d112791e441c27a6d529df/lib/Sbuild/ChrootUnshare.pm#L608
+ ZSTD="zstd -${SSTATE_ZSTD_CLEVEL} -T${ZSTD_THREADS}"
+ run_privileged \
+ tar -C ${ROOTFSDIR} -cpS $lopts ${ROOTFS_TAR_ATTR_FLAGS} . \
+ | $ZSTD > ${DEPLOY_SCHROOT}.tar.zst
+ # cleanup extracted rootfs
+ run_privileged rm -rf ${ROOTFSDIR}
+}
+
+do_sbuildchroot_deploy[network] = "${TASK_USE_SUDO}"
+do_sbuildchroot_deploy[dirs] += "${DEPLOY_DIR}/schroot-${SBUILD_VARIANT}"
+python do_sbuildchroot_deploy() {
+ if d.getVar('ISAR_CHROOT_MODE') == 'unshare':
+ bb.build.exec_func('sbuildchroot_deploy_tar', d)
+ else:
+ bb.build.exec_func('sbuildchroot_deploy_tree', d)
+}
addtask sbuildchroot_deploy before do_build after do_rootfs
--
2.53.0

--
You received this message because you are subscribed to the Google Groups "isar-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to isar-users+unsubscribe@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/isar-users/20260407142310.2327696-11-felix.moessbauer%40siemens.com.