Skip to main content
added 39 characters in body
Source Link

[This doesn't work in Ubuntu 24.04]

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably worth consideration for systems prior to Ubuntu 23.10. (On the other hand, this solution propagates ^C properly and bwrap doesn't)

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably worth consideration for systems prior to Ubuntu 23.10. (On the other hand, this solution propagates ^C properly and bwrap doesn't)

[This doesn't work in Ubuntu 24.04]

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably worth consideration for systems prior to Ubuntu 23.10. (On the other hand, this solution propagates ^C properly and bwrap doesn't)

added 76 characters in body
Source Link

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably worth consideration for systems prior to Ubuntu 23.10. (On the other hand, this solution propagates ^C properly and bwrap doesn't)

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably worth consideration for systems prior to Ubuntu 23.10.

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably worth consideration for systems prior to Ubuntu 23.10. (On the other hand, this solution propagates ^C properly and bwrap doesn't)

trim down to simple current best-solution answer
Source Link

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably bestworth consideration for systems prior to Ubuntu 23.10.

[EDIT: With updates below]However unlike bwrap, this solution based on unshare, properly propagates ^C for interactive CLI invocations.

You can't changechange the UID but with modern util-linux you can start off as non-root and then use nsenter to get root and make the mount.

# get a new mount and user namespace in background
unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash <&0 &

# now user nsenter without --preserve-credentials to do the mount
nsenter -U -m --target $! mount $SRC $DST -o rbind

# wait for unshare to complete
wait $!

This actually works, but needs error handling and something to cause the main namespace process to wait until the mount is done

Maybe just have another nsenter with the --preserve-credentials to run the process that needs the mount. There needs to be some way to keep the initial unshare active until the mount is done and the main process has started, and then close it so the whole thing can neatly collapse when the process finishes.

When I can find a way that doesn't use bash's coproc, I'll update my answer


As promised, the update

The needIt is necessary to start athe names-space with unshare, wait until it is ready, perform some mounts as fake-root, and when theythe mounts are ready, let the namespace run its tasks.

This takes two lots of synchronization, to wait until the namespace is ready before performing mounts, and to wait until mounts are ready before running the process.

This solution uses anonymous-ish pipes (using helper mk2pipe), and preserves stdio for the invoked process.

#! /bin/bash

# open an anonymous pipe, put the read handle in variable named $1 and the write handle in variable named $2
# WARNING: Be careful about closing write handles for read commands/subshells, or the read-handle may never get eof
# e.g. do this:
# good:   mk2pipe r w ; ( fd-close $w  ; cat <&${r} ; ) & echo hello >${w}
# better: mk2pipe r w ; ( fd-close $w  ; cat <&${r} ; ) & fd-close $r ; echo hello >${w}
mk2pipe() {
  mkfifo $(mktemp -u) && local header_pipe="$_" || return $?
  # the <> keeps it open both ways long enough for us to open a read and a write handle without blocking
  eval exec "{_}<>'$header_pipe' {$1}<'$header_pipe' {$2}>'$header_pipe' {_}<&-"
  set $?
  rm -f "$header_pipe"
  return $?
}

in-mount() (
  local session session_pid mounts
  while test $# != 0
  do case "$1" in
     *=*) mounts+=("$1") ; shift ;;
     --) shift ; break ;;
     *) break;
     esac     
  done

  # 2 pipes for synchronization
  mk2pipe r1 w1 || return $?
  mk2pipe r2 w2 || return $?

  unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash --noprofile --norc -c "exec ${r1}<&- ${w2}>&- && echo \$\$ >&${w1} && exec ${w1}>&- || exit \$? ; read -u ${r2} && exec ${r2}<&- && exec \"\$@\"" unshare "$@" <&0 &
  session_pid=$!

  # close the ends we don't need
  eval "exec ${w1}>&- ${r2}<&-"
  # wait until namespaces are setup
  read -r -u ${r1} || return $?
  eval "exec ${r1}<&-"

  # mount as "root" via nsenter
  for mount in "${mounts[@]}"
  do nsenter -U -m --target $session_pid mount "${mount#*=}" "${mount%=*}" -o rbind || return $?
  done
  # signal that mounts are setup
  echo >&${w2} # trigger to start
  eval "exec ${w2}>&-"

  # wait for unshare to complete
  wait $session_pid
)
in-mountunshare [bind-mount] ... [--] command [arg] ...
in-mountunshare /build-dir=$PWD/build -- make build

You may prefer this in-mount function which runs the main process in the foreground with different signal handling effects:

in-mount() (
  local session session_pid mounts
  while test $# != 0
  do case "$1" in
     *=*) mounts+=("$1") ; shift ;;
     --) shift ; break ;;
     *) break;
     esac
  done

  # 2 pipes for synchronization
  mk2pipe r1 w1 || return $?
  mk2pipe r2 w2 || return $?

  (
    # close the ends we don't need
    eval "exec ${w1}>&- ${r2}<&-"
    # wait until namespaces are setup
 **Note:** this avoids readan -rimplicit -uexec ${r1}with: session_pid`"$@" ||; returnexit $?
    eval "exec ${r1}<&-"

    # mount as "root" via nsenter
` which is necessary for mount in "${mounts[@]}"
    do nsenter -U -m --target $session_pid mount "${mount#*=}" "${mount%=*}" -o rbind || return $?
    done
    #proper signal that mountspropagation areon setup^C
    echo >&${w2} # trigger to start
    eval "exec ${w2}>&-"
  ) &

  exec unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash --noprofile --norc -c "execWho ${r1}<&-would ${w2}>&-have &&thought echoan \$\$extra >&${w1}waiting &&process execlayer ${w1}>&-would ||be exitso \$useful? ; read -u ${r2} && exec ${r2}<&- && exec \"\$@\"" unshare "$@"
)

Here's a simpler version using coproc which doesn't need the m2pipe helper, using tips from https://stackoverflow.com/a/47213971. Tested on bash 5.1.15 and unshare from util-linux 2.39.1 under ubuntu 23.04

#! /bin/bash

in-mountunshare() (
  local session session_pid mounts
  while test $# != 0
  do case "$1" in
     *=*) mounts+=("$1") ; shift ;;
     --) shift ; break ;;
     *) break;
     esac
  done

  coproc mounter {
    # session_pid will be the same as PPID but we need to wait until namespaces are setup
    read -r session_pid || return $?
    exec 0<&-

    # mount as "root" via nsenter
    for mount in "${mounts[@]}"
    do # quit on error without writing to stdout
       nsenter -U -m --target $session_pid mount "${mount#*=}" "${mount%=*}" -o rbind || return $?
    done
    # signal that mounts are setup
    echo $?
  }

  exec {out}>>/dev/fd/${mounter[1]} {in}</dev/fd/${mounter[0]}
  # note avoiding implict exec with "$@" ; exit $? is neccessary for proper signal shutdown on ^C
  # who would have thought an extra waiting process layer would be so useful
  exec unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash --noprofile --norc -c "echo \$\$ >&${out} && exec ${out}>&- || exit \$? ; read -u ${in} && exec ${in}<&- && exec\"\$@\" \"\$@\""; exit $?" unshare "$@"
  exit $?
)

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably best.

[EDIT: With updates below]

You can't change the UID but you can start off as non-root and then use nsenter to get root and make the mount.

# get a new mount and user namespace in background
unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash <&0 &

# now user nsenter without --preserve-credentials to do the mount
nsenter -U -m --target $! mount $SRC $DST -o rbind

# wait for unshare to complete
wait $!

This actually works, but needs error handling and something to cause the main namespace process to wait until the mount is done

Maybe just have another nsenter with the --preserve-credentials to run the process that needs the mount. There needs to be some way to keep the initial unshare active until the mount is done and the main process has started, and then close it so the whole thing can neatly collapse when the process finishes.

When I can find a way that doesn't use bash's coproc, I'll update my answer


As promised, the update

The need is to start a names-space, wait until it is ready, perform some mounts as fake-root, and when they are ready, let the namespace run its tasks.

This takes two lots of synchronization, to wait until the namespace is ready before performing mounts, and to wait until mounts are ready before running the process.

This solution uses anonymous-ish pipes (using helper mk2pipe), and preserves stdio for the invoked process.

#! /bin/bash

# open an anonymous pipe, put the read handle in variable named $1 and the write handle in variable named $2
# WARNING: Be careful about closing write handles for read commands/subshells, or the read-handle may never get eof
# e.g. do this:
# good:   mk2pipe r w ; ( fd-close $w  ; cat <&${r} ; ) & echo hello >${w}
# better: mk2pipe r w ; ( fd-close $w  ; cat <&${r} ; ) & fd-close $r ; echo hello >${w}
mk2pipe() {
  mkfifo $(mktemp -u) && local header_pipe="$_" || return $?
  # the <> keeps it open both ways long enough for us to open a read and a write handle without blocking
  eval exec "{_}<>'$header_pipe' {$1}<'$header_pipe' {$2}>'$header_pipe' {_}<&-"
  set $?
  rm -f "$header_pipe"
  return $?
}

in-mount() (
  local session session_pid mounts
  while test $# != 0
  do case "$1" in
     *=*) mounts+=("$1") ; shift ;;
     --) shift ; break ;;
     *) break;
     esac     
  done

  # 2 pipes for synchronization
  mk2pipe r1 w1 || return $?
  mk2pipe r2 w2 || return $?

  unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash --noprofile --norc -c "exec ${r1}<&- ${w2}>&- && echo \$\$ >&${w1} && exec ${w1}>&- || exit \$? ; read -u ${r2} && exec ${r2}<&- && exec \"\$@\"" unshare "$@" <&0 &
  session_pid=$!

  # close the ends we don't need
  eval "exec ${w1}>&- ${r2}<&-"
  # wait until namespaces are setup
  read -r -u ${r1} || return $?
  eval "exec ${r1}<&-"

  # mount as "root" via nsenter
  for mount in "${mounts[@]}"
  do nsenter -U -m --target $session_pid mount "${mount#*=}" "${mount%=*}" -o rbind || return $?
  done
  # signal that mounts are setup
  echo >&${w2} # trigger to start
  eval "exec ${w2}>&-"

  # wait for unshare to complete
  wait $session_pid
)
in-mount [bind-mount] ... [--] command [arg] ...
in-mount /build-dir=$PWD/build -- make build

You may prefer this in-mount function which runs the main process in the foreground with different signal handling effects:

in-mount() (
  local session session_pid mounts
  while test $# != 0
  do case "$1" in
     *=*) mounts+=("$1") ; shift ;;
     --) shift ; break ;;
     *) break;
     esac
  done

  # 2 pipes for synchronization
  mk2pipe r1 w1 || return $?
  mk2pipe r2 w2 || return $?

  (
    # close the ends we don't need
    eval "exec ${w1}>&- ${r2}<&-"
    # wait until namespaces are setup
    read -r -u ${r1} session_pid || return $?
    eval "exec ${r1}<&-"

    # mount as "root" via nsenter
    for mount in "${mounts[@]}"
    do nsenter -U -m --target $session_pid mount "${mount#*=}" "${mount%=*}" -o rbind || return $?
    done
    # signal that mounts are setup
    echo >&${w2} # trigger to start
    eval "exec ${w2}>&-"
  ) &

  exec unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash --noprofile --norc -c "exec ${r1}<&- ${w2}>&- && echo \$\$ >&${w1} && exec ${w1}>&- || exit \$? ; read -u ${r2} && exec ${r2}<&- && exec \"\$@\"" unshare "$@"
)

Here's a simpler version using coproc which doesn't need the m2pipe helper, using tips from https://stackoverflow.com/a/47213971. Tested on bash 5.1.15 and unshare from util-linux 2.39.1 under ubuntu 23.04

#! /bin/bash

in-mount() (
  local session session_pid mounts
  while test $# != 0
  do case "$1" in
     *=*) mounts+=("$1") ; shift ;;
     --) shift ; break ;;
     *) break;
     esac
  done

  coproc mounter {
    # session_pid will be the same as PPID but we need to wait until namespaces are setup
    read -r session_pid || return $?
    exec 0<&-

    # mount as "root" via nsenter
    for mount in "${mounts[@]}"
    do # quit on error without writing to stdout
       nsenter -U -m --target $session_pid mount "${mount#*=}" "${mount%=*}" -o rbind || return $?
    done
    # signal that mounts are setup
    echo $?
  }

  exec {out}>>/dev/fd/${mounter[1]} {in}</dev/fd/${mounter[0]}
  exec unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash --noprofile --norc -c "echo \$\$ >&${out} && exec ${out}>&- || exit \$? ; read -u ${in} && exec ${in}<&- && exec \"\$@\"" unshare "$@"
  exit $?
)

Note: as bwrap is available in more places than util-linux > 2.39.1 right now, this answer using bwrap is probably worth consideration for systems prior to Ubuntu 23.10.

However unlike bwrap, this solution based on unshare, properly propagates ^C for interactive CLI invocations.

You can't change the UID but with modern util-linux you can start off as non-root and then use nsenter to get root and make the mount.

It is necessary to start the names-space with unshare, wait until it is ready, perform some mounts as fake-root, and when the mounts are ready, let the namespace run its tasks.

This takes two lots of synchronization, to wait until the namespace is ready before performing mounts, and to wait until mounts are ready before running the process.

in-unshare [bind-mount] ... [--] command [arg] ...
in-unshare /build-dir=$PWD/build -- make build

# **Note:** this avoids an implicit exec with: `"$@" ; exit $?` which is necessary for proper signal propagation on ^C
# Who would have thought an extra waiting process layer would be so useful?
in-unshare() (
  local session session_pid mounts
  while test $# != 0
  do case "$1" in
     *=*) mounts+=("$1") ; shift ;;
     --) shift ; break ;;
     *) break;
     esac
  done

  coproc mounter {
    # session_pid will be the same as PPID but we need to wait until namespaces are setup
    read -r session_pid || return $?
    exec 0<&-

    # mount as "root" via nsenter
    for mount in "${mounts[@]}"
    do # quit on error without writing to stdout
       nsenter -U -m --target $session_pid mount "${mount#*=}" "${mount%=*}" -o rbind || return $?
    done
    # signal that mounts are setup
    echo $?
  }

  exec {out}>>/dev/fd/${mounter[1]} {in}</dev/fd/${mounter[0]}
  # note avoiding implict exec with "$@" ; exit $? is neccessary for proper signal shutdown on ^C
  # who would have thought an extra waiting process layer would be so useful
  exec unshare --mount --user --map-user=$(id -u) --map-group=$(id -g) --map-users=auto --map-groups=auto --keep-caps --setgroups allow /bin/bash --noprofile --norc -c "echo \$\$ >&${out} && exec ${out}>&- || exit \$? ; read -u ${in} && exec ${in}<&- && \"\$@\" ; exit $?" unshare "$@"
  exit $?
)
add exec to remove need for better signal propagation
Source Link
Loading
added 180 characters in body
Source Link
Loading
coproc version
Source Link
Loading
typos
Source Link
Loading
added 1319 characters in body
Source Link
Loading
fully synchronised solution
Source Link
Loading
Source Link
Loading