summaryrefslogtreecommitdiffstats
path: root/core/includes/chroot.inc
blob: d5a404c57ac03a2e49d490f56a3237b200111db6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
#!/bin/bash
# -----------------------------------------------------------------------------
#
# Copyright (c) 2014..2018 bwLehrpool-Projektteam
#
# This program/file is free software distributed under the GPL version 2.
# See https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
#
# If you have any feedback please consult https://bwlehrpool.de and
# send your feedback to bwlehrpool@hs-offenburg.de.
#
# General information about bwLehrpool can be found at https://bwlehrpool.de
#
# -----------------------------------------------------------------------------
#
#			Common functions for chrooting
#
# -----------------------------------------------------------------------------

declare -rg CHROOT_TEMPDIR="${ROOT_DIR}/tmp/chroot"
declare -rg CHROOT_MOUNTDIR="${CHROOT_TEMPDIR}/rootmount"
declare -rg CHROOT_BINDDIR="${CHROOT_TEMPDIR}/rootbind"
declare -rg CHROOT_LOWERDIR="/"
declare -rg CHROOT_BINDMOUNTS="/dev /proc /sys /run"

# Helper for the helper to detect overlay filesystem name
chroot_detect_overlayfs() {
	declare -g OVERLAYFS_FSTYPES="$(grep -oE '\boverlay(fs)?$' /proc/filesystems)"
	if [ "$(<<< $OVERLAYFS_FSTYPES wc -w)" -ge 1 ]; then
		# more than one overlayfs support in /proc/filesystems
		# either one should work, will try them all later
		pdebug "Found overlayfs types: $OVERLAYFS_FSTYPES"
		readonly OVERLAYFS_FSTYPES
		return 0
	fi
	unset OVERLAYFS_FSTYPES
	return 1
}
# Helper to make sure we can use overlayfs
chroot_init_overlayfs() {
	# check if we already support it
	chroot_detect_overlayfs && return 0

	# nothing found, try to load kernel module
	pwarning "No overlayfs found in /proc/filesystems, trying to load module..."
	for NAME in overlay overlayfs; do
		if modprobe "${NAME}"; then
			chroot_detect_overlayfs && break
		fi
	done
	if [ -z "$OVERLAYFS_FSTYPES" ]; then
		perror "Could not initialize overlayfs!"
	fi
}

# Helper function to setup the directory structure
chroot_prepare_dirs() {
	# first check if CHROOT_TEMPDIR exists
	if [ -d "${CHROOT_TEMPDIR}" ]; then
		# try to umount and rmdir CHROOT_MOUNTDIR
		umount "${CHROOT_MOUNTDIR}" 2>/dev/null
		if [ -d "${CHROOT_MOUNTDIR}" ]; then
			rmdir "${CHROOT_MOUNTDIR}" || perror "Could not remove CHROOT_MOUNTDIR '${CHROOT_MOUNTDIR}', meaning it has stuff in it. Aborting..."
		fi

		# try to umount and rmdir CHROOT_BINDDIR
		umount "${CHROOT_BINDDIR}" 2>/dev/null
		if [ -d "${CHROOT_BINDDIR}" ]; then
		 	rmdir "${CHROOT_BINDDIR}" || perror "Could not remove CHROOT_BINDDIR '${CHROOT_BINDDIR}', meaning it has stuff in it. Aborting..."
		fi

		# try to rmdir CHROOT_TEMPDIR
		if [ -d "${CHROOT_TEMPDIR}" ]; then
			rmdir "${CHROOT_TEMPDIR}" || perror "Could not remove CHROOT_TEMPDIR '${CHROOT_TEMPDIR}', meaning it has stuff in it. Aborting..."
		fi
	fi

	mkdir -p "${CHROOT_TEMPDIR}" || perror "Could not create base directory for mount directories $CHROOT_TEMPDIR."
	for DIR in "${CHROOT_BINDDIR}" "${CHROOT_MOUNTDIR}"; do
		mkdir -p "${DIR}" || perror "Could not create directory for mount directory $DIR."
	done
}

# Helper to mount the overlay structure:
# - bind mount system / to CHROOT_BINDDIR and make it read-only
# - make an overlay from CHROOT_LOWERDIR CHROOT_UPPERDIR
# - bind mount additional pseudo-fs (as given in CHROOT_BINDMOUNTS)
chroot_prepare_mounts() {

	# first mount / on CHROOT_BINDDIR and remount read-only
	mount -o bind "${CHROOT_LOWERDIR}" "${CHROOT_BINDDIR}" \
			|| perror "Could not bind-mount CHROOT_LOWERDIR '$CHROOT_LOWERDIR' to CHROOT_BINDDIR '$CHROOT_BINDDIR'."
	mount -o remount,ro,bind "${CHROOT_BINDDIR}"		|| perror "Could not remount CHROOT_BINDDIR '$CHROOT_BINDDIR' read-only."

	# check that it really is read-only
	[ "x$(mount | grep -E "^/ on ${CHROOT_BINDDIR}" | grep -v '\(.*ro.*\)')" != "x" ] \
			&& perror "CHROOT_BINDDIR '${CHROOT_BINDDIR}' is not read-only! Aborting..."

	# Note: The overlay fs mount syntax seems to be changed between Ubuntu 14.04.2 and 14.04.3 (Kernel 3.13 and 3.19). Instead of
	# checking overlay-modinfo (which may fail if overlayfs is not incorporated as module) or kernel versions, we simply try to
	# mount 'old school' first and then, if that fails, the new way to mount with workdir. See differences in mount syntax below.
	pinfo "Now mounting overlayfs. Trying old mount syntax (up to Kernel 3.13) ..."
	for OVERLAYFS_NAME in ${OVERLAYFS_FSTYPES}; do
		mount -t "${OVERLAYFS_NAME}" "${OVERLAYFS_NAME}" \
			-o lowerdir="${CHROOT_LOWERDIR}",upperdir="${CHROOT_UPPERDIR}" \
			"${CHROOT_MOUNTDIR}" 2>/dev/null
		if [ $? -ne 0 ]; then
			pinfo "Old mount syntax failed. Trying new mount syntax (Kernel 3.19+) ..."
			# We have to use a overlayfs workdir which _must_ be in the same filesystem as CHROOT_UPPERDIR. So
			# we traverse to the directory below CHROOT_UPPERDIR and mkdir/mktemp a workdir there. In the possible
			# case that CHROOT_UPPERDIR is the root dir of a filesystem there's nothing we can do but exit.
			CHROOT_WORKDIR="$(dirname ${CHROOT_UPPERDIR})/workdir-$MODULE"
			mkdir -p "$CHROOT_WORKDIR"
			if [ -z "$CHROOT_WORKDIR" ] || ! [ -d "$CHROOT_WORKDIR" ]; then
				perror "Could not mkdir overlayfs workdir $CHROOT_WORKDIR for new overlayfs mount syntax."
			fi
			# Now we try to mount the overlayfs in the new fashion:
			lsof -n > /tmp/bbboboboooo
			mount -v -t "${OVERLAYFS_NAME}" "${OVERLAYFS_NAME}" \
				-o lowerdir="${CHROOT_BINDDIR}",upperdir="${CHROOT_UPPERDIR}",workdir="${CHROOT_WORKDIR}" \
			       	"${CHROOT_MOUNTDIR}" \
					|| perror "Could not mount (overlayfs) $CHROOT_BINDDIR, $CHROOT_UPPERDIR, ${CHROOT_WORKDIR} to $CHROOT_MOUNTDIR."
			pinfo "New overlayfs mount syntax has worked, commencing."
		else
			pinfo "Old overlayfs mount syntax has worked, commencing."
		fi
	done

	#  mount pseudo-filesystems
	for DIR in $CHROOT_BINDMOUNTS; do
		mount -o bind "${DIR}" "${CHROOT_MOUNTDIR}/${DIR}" \
			|| perror "Could not bind mount '$DIR' into CHROOT_MOUNTDIR/DIR '$CHROOT_MOUNTDIR/$DIR'."
	done
}

# Helper to generate the mighty autoexec.bat
chroot_gen_autoexec() {
	# create the script to be automatically executed.
	cat >"${CHROOT_MOUNTDIR}/autoexec.bat"<<-EOF
		#!/bin/bash
		#######################################################
		#                                                     #
		#                     Warning!                        #
		#                                                     #
		#  This file is only meant to be executed within      #
		#  the specially chrooted mltk building environment.  #
		#                                                     #
		#  Do NOT execute it if you are not sure what you do, #
		#  it may be very harmful if being run in a normal    #
		#  system environment!                                #
		#                                                     #
		#######################################################
		echo "chroot started successfully."
	EOF

	# dump the piped input to it
	cat >> "${CHROOT_MOUNTDIR}/autoexec.bat"

	# make it executable
	chmod +x "${CHROOT_MOUNTDIR}/autoexec.bat" || perror "Failed to make '${CHROOT_MOUNTDIR}/autoexec.bat' exeutable."
}

chroot_handle_whiteouts() {
	local WHITEOUT_LIST="${CHROOT_UPPERDIR}/overlay.whiteout.list"
	rm -f -- "$WHITEOUT_LIST"
	mkdir -p "$(dirname "$WHITEOUT_LIST")" || perror "Could not create $(dirname "$WHITEOUT_LIST")"
	pdebug "Searching for overlayfs-whiteouts ..."
	for WHITEOUT in $(find "$CHROOT_UPPERDIR" \( -type c -perm 0000 \) -o -lname "(overlay-whiteout)"); do
		pdebug "Whiteout found: $WHITEOUT"
		echo "/./${WHITEOUT#$CHROOT_UPPERDIR}" >> "$WHITEOUT_LIST"
		rm -f -- "$WHITEOUT" || perror "Could not delete whiteout $WHITEOUT!"
	done
	[ -s "$WHITEOUT_LIST" ] && pinfo "Whiteout list dumped to '${CHROOT_UPPERDIR}/overlay.whiteout.list'"
}

###############################################################################
#
#				MAIN FUNCTION
#
# Main function to be called from the outside
# Usage:
#	chroot_run <build_dir> < <code_to_exec_in_chroot>
#
# Example:
#	chroot_run /tmp/chroot_build <<-EOF
#		echo "This will be executed inside the chroot"
#	EOF
#
# It will run:
#	- chroot_prepare
#	- chroot $CHROOT_TEMPDIR/rootmount
#	- executes $CHROOT_TEMPDIR/rootmount/autoexec.bat
#	- chroot_cleanup
chroot_run() {
	# check args
	[ $# -eq 1 ] || perror "'chroot_run' requires exactly 1 parameter. Given $#.  Use 'chroot_run <build_dir>'"

	local CHROOT_UPPERDIR="$1"
	mkdir -p "$1" 

	# init overlayfs
	chroot_init_overlayfs || perror "Failed to initialize overlayfs with $?."
	# first prepare the dir structure
	chroot_prepare_dirs || perror "'chroot_prepare_dirs' failed with $?."
	chroot_prepare_mounts || perror "'chroot_prepare_mounts' failed with $?."

	# generate the code to be executed when chroot'ing
	chroot_gen_autoexec || perror "'chroot_gen_autoexec' failed with $?."

	# do the chroot
	exec 0>&8		# This redirection is used for debugging within a chroot
	chroot --userspec root:root "${CHROOT_MOUNTDIR}" /autoexec.bat
	local RET=$?
	if [ "$RET" -eq 0 ]; then
		pinfo "chroot executed '${CHROOT_MOUNTDIR}/autoexec.bat' succeeded."
	else
		perror "Failed to run '$CHROOT_MOUNTDIR/autoexec.bat' inside the chroot to '$CHROOT_MOUNTDIR' with error code: $RET"
	fi

	# handle whiteouts
	chroot_handle_whiteouts || perror "'chroot_handle_whiteouts' failed with error code: $?"

	# finally cleanup all the mounting stuff we did previously
	chroot_cleanup_mounts			|| perror "'chroot_cleanup' failed with $?."
}

###############################################################################
#
#				CLEANUP FUNCTIONS
#
# Helper to check if the given path is mounted
chroot_check_mount_point() {
	[ "$#" -eq 1 ] || perror "'chroot_check_mount_point' called with $# arguements, only 1 accepted."
	local MOUNT="$1"	
	if [ "x$(mount | grep "$(readlink -f $MOUNT)")" != "x" ]; then
		# still mounted
		pdebug "'$MOUNT' is mounted!"
		return 1
	else
		pdebug "'$MOUNT' is not mounted."
		return 0
	fi
}

# Helper to umount the given path
chroot_umount() {
	[ "$#" -eq 1 ] || perror "'chroot_umount' called with $# arguments, only 1 accepted."
	local MOUNT="$1"

	# check if MOUNT is mounted
	if ! chroot_check_mount_point "${MOUNT}"; then
		# still mounted
		if umount "${MOUNT}"; then
			pdebug "Successfully umounted '${MOUNT}'."
		else
			pwarning "Could not umount '${MOUNT}'! Trying again..."
			# now it gets ugly
			for i in `seq 1 5`; do
				umount -l "${MOUNT}" && return 0
			done
			perror "Could not umount '${MOUNT}' after 5 tries! This shouldn't happen. Check your scripts."
		fi
	else
		pdebug "'${MOUNT}' is not mounted."
	fi

	# better be safe than sorry
}

# Helper to cleanup the temporary mounts
chroot_cleanup_mounts() {
	local exe FILE pid tries
	local DOKILL=
	for tries in 1 2 0; do
		for exe in /proc/*/exe; do
			pid=${exe#/proc/}
			pid=${pid%/exe}
			FILE=$(readlink -f "$exe")
			if [ "${FILE#$CHROOT_TEMPDIR}" != "$FILE" ]; then
				pwarning "Killing $FILE ($pid)"
				kill $DOKILL "$pid"
				DOKILL="-9"
				kill "$pid"
			fi
		done
		[ -z "$DOSLEEP" ] && break
		sleep "$tries"
	done
	if [[ "$(mount | grep -c $CHROOT_TEMPDIR)" -gt 0 ]]; then	# No point in unmounting then...
		for tries in 1 2 3 4 5; do
			for DIR in $CHROOT_BINDMOUNTS; do
				umount "${CHROOT_MOUNTDIR}/${DIR}"
			done
			umount "${CHROOT_MOUNTDIR}"
			umount "${CHROOT_BINDDIR}"
		done
		for DIR in $CHROOT_BINDMOUNTS; do
			chroot_check_mount_point "$CHROOT_MOUNTDIR/$DIR" || perror "'$CHROOT_MOUNTDIR/$DIR' is still mounted, exiting before something bad happens..."
		done
	else
		pinfo "Nothing chroot-related is mounted - exiting."
	fi

}