#!/bin/sh
# Filename:      forensic-mark-readonly
# Purpose:       force block devices to read-only mode when booting with boot option read-only
# Authors:       grml-team (grml.org), (c) Michael Prokop <mika@grml.org>
# Bug-Reports:   see http://grml.org/bugs/
# License:       This file is licensed under the GPL v2 or any later version.
################################################################################

# check for read-only bootoption
if ! grep -q read-only /proc/cmdline ; then
  exit 0
fi

# see linux source -> Documentation/admin-guide/sysfs-rules.rst
get_blockdev_dir() {
  for dir in /sys/subsystem/block /sys/class/block /sys/block ; do
    [ -d "${dir}" ] && echo "${dir}" && return
  done
}

base() {
  echo "${1##*/}"
}

dir() {
  echo "${1%/*}"
}

is_ro() {
  # ASSUMPTION: Device paths can not contain spaces
  while [ "$#" -gt 0 ]; do
    [ "$(blockdev --getro "$1")" = "1" ] && return 0
    shift
  done
  return 1
}

if [ -z "${1:-}" ] ; then
  echo "Error: usage: <$0> <blockdevice>" >&2
  exit 1
fi

# accept /dev/foo from command line but also just "foo" from udev
case "$1" in
  /dev/*)
    BLOCK_DEVICE="$1"
    ;;
  *)
    BLOCK_DEVICE="/dev/$1"
    ;;
esac

SYS_DIR="$(get_blockdev_dir)"

patternsfile=
cleanup() {
  if [ -f "${patternsfile}" ] ; then
    rm -f "${patternsfile}"
  fi
}
trap cleanup EXIT

base_device=$(base "${BLOCK_DEVICE}")
if [ -n "${SYS_DIR}" ] && [ -n "${base_device}" ] ; then
  if [ -z "${base_device##loop[0-9]*}" ] ; then
    if [ -e "${SYS_DIR}"/"${base_device}"/loop/backing_file ] ; then
      backingfile=$(cat "${SYS_DIR}"/"${base_device}"/loop/backing_file)
      parent_devices=$(findmnt -n -o source --target "${backingfile}")

      # Check if either the backing file's block device or filesystem are read-only
      if is_ro "${parent_devices}" || findmnt -n -o OPTIONS --target "${backingfile}" | grep -qE '^ro,|,ro,|,ro$'; then
        blockdev --setro "${BLOCK_DEVICE}"
        logger -t forensic-mark-readonly "setting '${BLOCK_DEVICE}' with backing file (${backingfile}) (parent devices: '${parent_devices}') to read-only as its parent is present or mount point ($(findmnt -n -o TARGET --target "${backingfile}")) is read-only"
      elif [ "$(blockdev --getro "${BLOCK_DEVICE}")" = 0 ]; then
        blockdev --setrw "${BLOCK_DEVICE}"
        logger -t forensic-mark-readonly "setting '${BLOCK_DEVICE}' with backing file (${backingfile}) (parent devices: '${parent_devices}') to read-write because it is already set as read-only, but should be read-write"
      fi
      exit 0
    fi
  fi

  tmp_parents="$(readlink -f "${SYS_DIR}"/*/"${base_device}")"
  if [ -z "${tmp_parents}" ]; then
    tmp_parents=
    for DEV in "${SYS_DIR}"/*/holders/"${base_device}"; do
      [ -d "${DEV}" ] && tmp_parents="${tmp_parents:+${tmp_parents} }$(dir "${DEV}")"
    done
  fi
  # ASSUMPTION: There should be no spaces in device names.
  parent_devices=
  for tmp_parent in ${tmp_parents}; do
    if [ -d "${tmp_parent}" ] ; then
      tmp_parent=$(dir "${tmp_parent}")
      tmp_parent=$(base "${tmp_parent}")
      tmp_parent="/dev/${tmp_parent}"
      parent_devices="${parent_devices:+${parent_devices} }${tmp_parent}"
    fi
  done
  unset tmp_parent tmp_parents
fi

# support configuration file
if [ -r /etc/grml/forensic.conf ] ; then
  READONLY_MODE=""
  READONLY_IGNORE=""

  # shellcheck disable=SC1091
  . /etc/grml/forensic.conf

  if [ "${READONLY_MODE:-}" = "disable" ] ; then
    logger -t forensic-mark-readonly "not setting '${BLOCK_DEVICE}' to read-only as disabled via config"
    exit 0
  fi

  if [ -n "${READONLY_IGNORE:-}" ] ; then
    if [ -n "${parent_devices}" ] ; then
      patternsfile=$(mktemp forensic-mark-readonly.patterns.XXXXXX)
      echo "${parent_devices}" | sed "s| |\n|" > "${patternsfile}"
      if echo "${READONLY_IGNORE:-}" | grep -qw -F "${patternsfile}" ; then
        logger -t forensic-mark-readonly "not setting '${BLOCK_DEVICE}' (parent devices: '${parent_devices}') to read-only as its parent is present in ignore list"
        exit 0
      fi
    fi
    if echo "${READONLY_IGNORE:-}" | grep -qw "${BLOCK_DEVICE}" ; then
      logger -t forensic-mark-readonly "not setting '${BLOCK_DEVICE}' to read-only as present in ignore list"
      exit 0
    fi
  fi
fi

if is_ro "${BLOCK_DEVICE}" ; then
  logger -t forensic-mark-readonly "device ${BLOCK_DEVICE} already set to read-only mode, nothing to do"
elif [ -n "${parent_devices}" ] && ! is_ro "${parent_devices}" ; then
  logger -t forensic-mark-readonly "all parent devices (${parent_devices}) are set read-write, not modifying"
  logger -t forensic-mark-readonly "use blockdev --setro ${BLOCK_DEVICE} to set it manually"
else
  logger -t forensic-mark-readonly "setting ${BLOCK_DEVICE} [${ID_SERIAL}] to read-only"

  if blockdev --setro "${BLOCK_DEVICE}" ; then
    logger -t forensic-mark-readonly "|-> done; execute 'blockdev --setrw ${BLOCK_DEVICE}' to unlock"
  else
    logger -t forensic-mark-readonly "|-> error while executing blockdev: $(blockdev --setro "${BLOCK_DEVICE}" 2>&1)"
  fi
fi

## END OF FILE #################################################################
