aboutgitcodelistschat:MatrixIRC
path: root/filter.sh
blob: b3b85a248d0eb5172d07ebb6c9e8bbef9cb72a9c (plain) (tree)








































































































































































































































































                                                                                                  
#!/bin/sh -eu
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# SEITAN - Syscall Expressive Interpreter, Transformer and Notifier
#
# filter.sh - Build binary-search tree BPF program with SECCOMP_RET_USER_NOTIF
#
# Copyright (c) 2022 Red Hat GmbH
# Author: Stefano Brivio <sbrivio@redhat.com>

TMP="$(mktemp)"
IN="${@}"
OUT="filter.h"
OUT_NUMBERS="numbers.h"

HEADER="/* This file was automatically generated by $(basename ${0}) */

#ifndef AUDIT_ARCH_PPC64LE
#define AUDIT_ARCH_PPC64LE (AUDIT_ARCH_PPC64 | __AUDIT_ARCH_LE)
#endif"

HEADER_NUMBERS="/* This file was automatically generated by $(basename ${0}) */
struct syscall_numbers numbers[] = {"

FOOTER_NUMBERS="};"

# Prefix for each profile: check that 'arch' in seccomp_data is matching
PRE='
struct sock_filter @PROFILE@[] = {
	/* cppcheck-suppress badBitmaskCheck */
	BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
		 (offsetof(struct seccomp_data, arch))),
	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SEITAN_AUDIT_ARCH, 0, @KILL@),
	/* cppcheck-suppress badBitmaskCheck */
	BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
		 (offsetof(struct seccomp_data, nr))),

'

# Suffix for each profile: return actions
POST='	BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
	BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
};
'

# Syscall, @NR@: number, @ALLOW@: offset to RET_ALLOW, @NAME@: syscall name
CALL='	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, @NR@, @ALLOW@, 0), /* @NAME@ */'

# Binary search tree node or leaf, @NR@: value, @R@: right jump, @L@: left jump
BST='	BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, @NR@, @R@, @L@),'

# cleanup() - Remove temporary file if it exists
cleanup() {
	rm -f "${TMP}"
}
#trap "cleanup" EXIT

# sub() - Substitute in-place file line with processed template line
# $1:	Line number
# $2:	Template name (variable name)
# $@:	Replacement for @KEY@ in the form KEY:value
sub() {
	IFS=
	__line_no="${1}"
	__template="$(eval printf '%s' "\${${2}}")"
	shift; shift

	sed -i "${__line_no}s#.*#${__template}#" "${TMP}"

	IFS=' '
	for __def in ${@}; do
		__key="@${__def%%:*}@"
		__value="${__def#*:}"
		sed -i "${__line_no}s/${__key}/${__value}/" "${TMP}"
	done
	unset IFS
}

# finish() - Finalise header file from temporary files with prefix and suffix
# $1:	Variable name of prefix
# $@:	Replacements for prefix variable
finish() {
	IFS=
	__out="$(eval printf '%s' "\${${1}}")"
	shift

	IFS=' '
	for __def in ${@}; do
		__key="@${__def%%:*}@"
		__value="${__def#*:}"
		__out="$(printf '%s' "${__out}" | sed "s#${__key}#${__value}#")"
	done

	printf '%s\n' "${__out}" >> "${OUT}"
	cat "${TMP}" >> "${OUT}"
	rm "${TMP}"
	printf '%s' "${POST}" >> "${OUT}"
	unset IFS
}

# log2() - Binary logarithm
# $1:	Operand
log2() {
	__x=-1
	__y=${1}
	while [ ${__y} -gt 0 ]; do : $((__y >>= 1)); __x=$((__x + 1)); done
	echo ${__x}
}

# syscall_nr - Get syscall number from compiler, also note in numbers.h
# $1:	Name of syscall
syscall_nr() {
	__in="$(printf "#include <asm-generic/unistd.h>\n#include <sys/syscall.h>\n__NR_%s" ${1})"
	__out="$(echo "${__in}" | cc -E -xc - -o - | tail -1)"
	[ "${__out}" = "__NR_$1" ] && return 1

	# Output might be in the form "(x + y)" (seen on armv6l, armv7l)
	__out="$(eval echo $((${__out})))"
	echo "${__out}"

	printf "\t{ \"%s\",\t\t%i },\n" "${1}" "${__out}" >> "${OUT_NUMBERS}"
}

filter() {
	__filtered=
	for __c in ${@}; do
		__arch_match=0
		case ${__c} in
		*:*)
			case ${__c} in
			$(uname -m):*)
				__arch_match=1
				__c=${__c##*:}
				;;
			esac
			;;
		*)
			__arch_match=1
			;;
		esac
		[ ${__arch_match} -eq 0 ] && continue

		IFS='| '
		__found=0
		for __name in ${__c}; do
			syscall_nr "${__name}" >/dev/null && __found=1 && break
		done
		unset IFS

		if [ ${__found} -eq 0 ]; then
			echo
			echo "Warning: no syscall number for ${__c}" >&2
			echo "  none of these syscalls will be allowed" >&2
			continue
		fi

		__filtered="${__filtered} ${__name}"
	done

	echo "${__filtered}" | tr ' ' '\n' | sort -u
}

# gen_profile() - Build struct sock_filter for a single profile
# $1:	Profile name
# $@:	Names of allowed system calls, amount padded to next power of two
gen_profile() {
	__profile="${1}"
	shift

	__statements_calls=${#}
	__bst_levels=$(log2 $(( __statements_calls / 4 )) )
	__statements_bst=$(( __statements_calls / 4 - 1 ))
	__statements=$((__statements_calls + __statements_bst))

	[ ${__bst_levels} -eq 0 ] && __statements_bst=0
	for __i in $(seq 1 ${__statements_bst} ); do
		echo -1 >> "${TMP}"
	done

	for __i in $(seq 1 ${__statements_calls} ); do
		__syscall_name="$(eval echo \${${__i}})"
		if ! syscall_nr ${__syscall_name} >> "${TMP}"; then
			echo "Cannot get syscall number for ${__syscall_name}"
			exit 1
		fi
		eval __syscall_nr_$(tail -1 "${TMP}")="${__syscall_name}"
	done
	sort -go "${TMP}" "${TMP}"

	__level_nodes=1
	__distance=$(( __statements_calls / 2 ))
	__ll=0
	__line=1
	for __level in $(seq 1 $(( __bst_levels - 1 )) ); do
		# Nodes
		__cmp_pos=${__distance}

		for __node in $(seq 1 ${__level_nodes}); do
			__cmp_line=$(( __statements_bst + __cmp_pos ))
			__lr=$(( __ll + 1 ))
			__nr="$(sed -n ${__cmp_line}p "${TMP}")"

			sub ${__line} BST "NR:${__nr}" "L:${__ll}" "R:${__lr}"

			__ll=${__lr}
			__line=$(( __line + 1 ))
			__cmp_pos=$(( __cmp_pos + __distance * 2 ))
		done

		__distance=$(( __distance / 2 ))
		__level_nodes=$(( __level_nodes * 2 ))
	done

	# Leaves
	if [ ${__bst_levels} -eq 0 ]; then
		__ll=0
	else
		__ll=$(( __level_nodes - 1 ))
	fi
	__lr=$(( __ll + __distance - 1 ))
	__cmp_pos=${__distance}

	for __leaf in $(seq 1 ${__level_nodes}); do
		__cmp_line=$(( __statements_bst + __cmp_pos ))
		__nr="$(sed -n ${__cmp_line}p "${TMP}")"
		sub ${__line} BST "NR:${__nr}" "L:${__ll}" "R:${__lr}"

		__ll=$(( __lr + __distance - 1 ))
		__lr=$(( __ll + __distance))
		__line=$(( __line + 1 ))
		__cmp_pos=$(( __cmp_pos + __distance * 2 ))
	done

	# Calls
	[ ${__bst_levels} -eq 0 ] && __statements_bst=$((__statements_bst + 1))
	for __i in $(seq $(( __statements_bst + 1 )) ${__statements}); do
		__nr="$(sed -n ${__i}p "${TMP}")"
		eval __name="\${__syscall_nr_${__nr}}"
		__allow=$(( __statements - __i + 1))
		sub ${__i} CALL "NR:${__nr}" "NAME:${__name}" "ALLOW:${__allow}"
	done
	finish PRE "PROFILE:${__profile}" "KILL:$(( __statements + 1))"
}

printf '%s\n' "${HEADER}" > "${OUT}"
printf '%s\n' "${HEADER_NUMBERS}" > "${OUT_NUMBERS}"
__profiles="${IN}"
for __p in ${__profiles}; do
	__calls="$(sed -n 's/^\([^# \t]\{1,\}\).*/\1/p' "${__p}")"
	__calls="$(filter ${__calls})"
	echo "seccomp profile ${__p} handles: ${__calls}" | tr '\n' ' ' | fmt -t

	# Pad here to keep gen_profile() "simple"
	__count=0
	for __c in ${__calls}; do __count=$(( __count + 1 )); done
	__padded=$(( 1 << (( $(log2 ${__count}) + 1 )) ))
	for __i in $( seq ${__count} $(( __padded - 1 )) ); do
		__calls="${__calls} read"
	done

	gen_profile "${__p}" ${__calls}
done

printf '%s\n' "${FOOTER_NUMBERS}" >> "${OUT_NUMBERS}"