import: check exact size of copied data
The import will error out if there is not enough data, or too much data provided.
This commit is contained in:
parent
63ac952803
commit
e9b97e42b1
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# This Admin API call is implemented as a custom script, instead of dumb
|
# This Admin API call is implemented as a custom script, instead of dumb
|
||||||
# passthrough to qubesd because it may get huge amount of data (whole root.img
|
# passthrough to qubesd because it may get huge amount of data (whole root.img
|
||||||
@ -32,39 +32,64 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# make dd output consistent
|
||||||
|
export LC_ALL=C
|
||||||
|
|
||||||
# use temporary file, because env variables deal poorly with \0 inside
|
# use temporary file, because env variables deal poorly with \0 inside
|
||||||
tmpfile=$(mktemp)
|
tmpfile=$(mktemp)
|
||||||
trap "rm -f $tmpfile" EXIT
|
trap 'rm -f $tmpfile' EXIT
|
||||||
|
|
||||||
requested_size=""
|
requested_size=""
|
||||||
if [[ ${0##*/} == admin.vm.volume.ImportWithSize ]]; then
|
if [[ "${0##*/}" == admin.vm.volume.ImportWithSize ]]; then
|
||||||
read requested_size
|
read -r requested_size
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n "$requested_size" | qubesd-query -c /var/run/qubesd.internal.sock \
|
echo -n "$requested_size" | qubesd-query -c /var/run/qubesd.internal.sock \
|
||||||
"$QREXEC_REMOTE_DOMAIN" \
|
"$QREXEC_REMOTE_DOMAIN" \
|
||||||
"internal.vm.volume.ImportBegin" \
|
"internal.vm.volume.ImportBegin" \
|
||||||
"$QREXEC_REQUESTED_TARGET" \
|
"$QREXEC_REQUESTED_TARGET" \
|
||||||
"$1" >$tmpfile
|
"$1" >"$tmpfile"
|
||||||
|
|
||||||
# exit if qubesd returned an error (not '0\0')
|
# exit if qubesd returned an error (not '0\0')
|
||||||
if [ "$(head -c 2 $tmpfile | xxd -p)" != "3000" ]; then
|
if [ "$(head -c 2 "$tmpfile" | xxd -p)" != "3000" ]; then
|
||||||
cat "$tmpfile"
|
cat "$tmpfile"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
size=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 1)
|
size=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 1)
|
||||||
path=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 2)
|
path=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 2)
|
||||||
|
|
||||||
|
error=""
|
||||||
|
|
||||||
# now process stdin into this path
|
# now process stdin into this path
|
||||||
if sudo dd bs=128K of="$path" count="$size" iflag=count_bytes,fullblock \
|
if ! sudo dd bs=128K of="$path" count="$size" iflag=count_bytes,fullblock \
|
||||||
conv=sparse,notrunc,nocreat,fdatasync status=none; then
|
conv=sparse,notrunc,nocreat,fdatasync 2>"$tmpfile"; then
|
||||||
|
error="error copying data"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Examine dd's output and check if number of bytes copied matches
|
||||||
|
if [ -z "$error" ]; then
|
||||||
|
bytes_copied=$(tail -n1 "$tmpfile" | cut -d ' ' -f 1)
|
||||||
|
if [ "$bytes_copied" -ne "$size" ]; then
|
||||||
|
error="not enough data (copied $bytes_copied bytes, expected $size bytes)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there is nothing more to be read from stdin
|
||||||
|
if [ -z "$error" ]; then
|
||||||
|
if ! dd of="$tmpfile" bs=1 count=1 status=none || \
|
||||||
|
[ "$(stat -c %s "$tmpfile")" -ne 0 ]; then
|
||||||
|
error="too much data (expected $size bytes)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$error" ]; then
|
||||||
status="ok"
|
status="ok"
|
||||||
else
|
else
|
||||||
status="fail"
|
status="fail\n$error"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# send status notification to qubesd, and pass its response to the caller
|
# send status notification to qubesd, and pass its response to the caller
|
||||||
echo -n "$status" | qubesd-query -c /var/run/qubesd.internal.sock \
|
echo -ne "$status" | qubesd-query -c /var/run/qubesd.internal.sock \
|
||||||
"$QREXEC_REMOTE_DOMAIN" \
|
"$QREXEC_REMOTE_DOMAIN" \
|
||||||
"internal.vm.volume.ImportEnd" \
|
"internal.vm.volume.ImportEnd" \
|
||||||
"$QREXEC_REQUESTED_TARGET" \
|
"$QREXEC_REQUESTED_TARGET" \
|
||||||
|
@ -118,6 +118,8 @@ class QubesInternalAPI(qubes.api.AbstractQubesAPI):
|
|||||||
This is second half of admin.vm.volume.Import handling. It is called
|
This is second half of admin.vm.volume.Import handling. It is called
|
||||||
when actual import is finished. Response from this method is sent do
|
when actual import is finished. Response from this method is sent do
|
||||||
the client (as a response for admin.vm.volume.Import call).
|
the client (as a response for admin.vm.volume.Import call).
|
||||||
|
|
||||||
|
The payload is either 'ok', or 'fail\n<error message>'.
|
||||||
'''
|
'''
|
||||||
self.enforce(self.arg in self.dest.volumes.keys())
|
self.enforce(self.arg in self.dest.volumes.keys())
|
||||||
success = untrusted_payload == b'ok'
|
success = untrusted_payload == b'ok'
|
||||||
@ -134,7 +136,12 @@ class QubesInternalAPI(qubes.api.AbstractQubesAPI):
|
|||||||
success=success)
|
success=success)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
raise qubes.exc.QubesException('Data import failed')
|
error = ''
|
||||||
|
parts = untrusted_payload.decode('ascii').split('\n', 1)
|
||||||
|
if len(parts) > 1:
|
||||||
|
error = parts[1]
|
||||||
|
raise qubes.exc.QubesException(
|
||||||
|
'Data import failed: {}'.format(error))
|
||||||
|
|
||||||
@qubes.api.method('internal.SuspendPre', no_payload=True)
|
@qubes.api.method('internal.SuspendPre', no_payload=True)
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -1403,6 +1403,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
|
|||||||
'qubes.tests.api_admin',
|
'qubes.tests.api_admin',
|
||||||
'qubes.tests.api_misc',
|
'qubes.tests.api_misc',
|
||||||
'qubes.tests.api_internal',
|
'qubes.tests.api_internal',
|
||||||
|
'qubes.tests.rpc_import',
|
||||||
):
|
):
|
||||||
tests.addTests(loader.loadTestsFromName(modname))
|
tests.addTests(loader.loadTestsFromName(modname))
|
||||||
|
|
||||||
|
@ -1796,6 +1796,28 @@ netvm default=True type=vm \n'''
|
|||||||
self.assertEventFired(
|
self.assertEventFired(
|
||||||
self.emitter, 'admin-permission:admin.vm.volume.ImportWithSize')
|
self.emitter, 'admin-permission:admin.vm.volume.ImportWithSize')
|
||||||
|
|
||||||
|
def test_510_vm_volume_import_end_success(self):
|
||||||
|
import_data_end_mock, self.vm.storage.import_data_end = \
|
||||||
|
self.coroutine_mock()
|
||||||
|
self.call_internal_mgmt_func(
|
||||||
|
b'internal.vm.volume.ImportEnd', b'test-vm1', b'private',
|
||||||
|
payload=b'ok')
|
||||||
|
self.assertEqual(import_data_end_mock.mock_calls, [
|
||||||
|
unittest.mock.call('private', success=True)
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_510_vm_volume_import_end_failure(self):
|
||||||
|
import_data_end_mock, self.vm.storage.import_data_end = \
|
||||||
|
self.coroutine_mock()
|
||||||
|
with self.assertRaisesRegexp(
|
||||||
|
qubes.exc.QubesException, 'error message'):
|
||||||
|
self.call_internal_mgmt_func(
|
||||||
|
b'internal.vm.volume.ImportEnd', b'test-vm1', b'private',
|
||||||
|
payload=b'fail\nerror message')
|
||||||
|
self.assertEqual(import_data_end_mock.mock_calls, [
|
||||||
|
unittest.mock.call('private', success=False)
|
||||||
|
])
|
||||||
|
|
||||||
def setup_for_clone(self):
|
def setup_for_clone(self):
|
||||||
self.pool = unittest.mock.MagicMock()
|
self.pool = unittest.mock.MagicMock()
|
||||||
self.app.pools['test'] = self.pool
|
self.app.pools['test'] = self.pool
|
||||||
|
180
qubes/tests/rpc_import.py
Normal file
180
qubes/tests/rpc_import.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
|
||||||
|
#
|
||||||
|
# The Qubes OS Project, https://www.qubes-os.org/
|
||||||
|
#
|
||||||
|
# Copyright (C) 2020 Paweł Marczewski <pawel@invisiblethingslab.com>
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
import qubes.tests
|
||||||
|
|
||||||
|
|
||||||
|
class TestRpcImport(qubes.tests.QubesTestCase):
|
||||||
|
'''
|
||||||
|
Tests for qubes-rpc/admin.vm.volume.Import script.
|
||||||
|
|
||||||
|
It is a shell script that calls internal API methods via qubesd-query.
|
||||||
|
These tests mock all the calls.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
QUBESD_QUERY = '''\
|
||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
method=$4
|
||||||
|
echo "$@" > command-$method
|
||||||
|
cat > payload-$method
|
||||||
|
cat response-$method
|
||||||
|
'''
|
||||||
|
|
||||||
|
RPC_FILE_PATH = os.path.abspath(os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
'../../qubes-rpc/admin.vm.volume.Import'))
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.tmpdir = tempfile.mkdtemp()
|
||||||
|
self.addCleanup(shutil.rmtree, self.tmpdir)
|
||||||
|
with open(os.path.join(self.tmpdir, 'qubesd-query'), 'w') \
|
||||||
|
as qubesd_query_f:
|
||||||
|
qubesd_query_f.write(self.QUBESD_QUERY)
|
||||||
|
os.chmod(os.path.join(self.tmpdir, 'qubesd-query'), 0o700)
|
||||||
|
|
||||||
|
shutil.copy(
|
||||||
|
self.RPC_FILE_PATH,
|
||||||
|
os.path.join(self.tmpdir, 'admin.vm.volume.Import'))
|
||||||
|
shutil.copy(
|
||||||
|
self.RPC_FILE_PATH,
|
||||||
|
os.path.join(self.tmpdir, 'admin.vm.volume.ImportWithSize'))
|
||||||
|
|
||||||
|
# pylint:disable=invalid-name
|
||||||
|
def mockMethod(self, method, response):
|
||||||
|
with open(os.path.join(self.tmpdir, 'response-' + method), 'wb') \
|
||||||
|
as response_f:
|
||||||
|
response_f.write(response)
|
||||||
|
|
||||||
|
# pylint:disable=invalid-name
|
||||||
|
def assertMethodCalled(self, method, arg, expected_payload=b''):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.tmpdir, 'command-' + method), 'rb') \
|
||||||
|
as command_f:
|
||||||
|
command = command_f.read()
|
||||||
|
with open(os.path.join(self.tmpdir, 'payload-' + method), 'rb') \
|
||||||
|
as payload_f:
|
||||||
|
payload = payload_f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.fail('{} was not called'.format(method))
|
||||||
|
|
||||||
|
self.assertListEqual(command.decode().split(), [
|
||||||
|
'-c', '/var/run/qubesd.internal.sock',
|
||||||
|
'remote', method, 'target', arg
|
||||||
|
])
|
||||||
|
self.assertEqual(payload, expected_payload)
|
||||||
|
|
||||||
|
# pylint:disable=invalid-name
|
||||||
|
def assertFileData(self, path, expected_data):
|
||||||
|
with open(path, 'rb') as data_f:
|
||||||
|
data = data_f.read()
|
||||||
|
self.assertEquals(data, expected_data)
|
||||||
|
|
||||||
|
def setup_import(self, size):
|
||||||
|
self.target = os.path.join(self.tmpdir, 'target')
|
||||||
|
os.mknod(self.target)
|
||||||
|
|
||||||
|
self.mockMethod(
|
||||||
|
'internal.vm.volume.ImportBegin',
|
||||||
|
'\x30\x00{} {}'.format(size, self.target).encode())
|
||||||
|
|
||||||
|
self.mockMethod(
|
||||||
|
'internal.vm.volume.ImportEnd',
|
||||||
|
b'\x30\x00import-end')
|
||||||
|
|
||||||
|
def run_rpc(self, command, arg, data):
|
||||||
|
with open(os.path.join(self.tmpdir, 'data'), 'w+b') as data_f:
|
||||||
|
data_f.write(data)
|
||||||
|
data_f.seek(0)
|
||||||
|
env = {
|
||||||
|
'PATH': self.tmpdir + ':' + os.getenv('PATH'),
|
||||||
|
'QREXEC_REMOTE_DOMAIN': 'remote',
|
||||||
|
'QREXEC_REQUESTED_TARGET': 'target',
|
||||||
|
}
|
||||||
|
output = subprocess.check_output(
|
||||||
|
[command, arg],
|
||||||
|
env=env,
|
||||||
|
cwd=self.tmpdir,
|
||||||
|
stdin=data_f
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(output, b'\x30\x00import-end')
|
||||||
|
|
||||||
|
def test_00_import(self):
|
||||||
|
data = b'abcd' * 1024
|
||||||
|
size = len(data)
|
||||||
|
self.setup_import(size)
|
||||||
|
|
||||||
|
self.run_rpc('admin.vm.volume.Import', 'volume', data)
|
||||||
|
|
||||||
|
self.assertMethodCalled('internal.vm.volume.ImportBegin', 'volume')
|
||||||
|
self.assertMethodCalled('internal.vm.volume.ImportEnd', 'volume',
|
||||||
|
b'ok')
|
||||||
|
self.assertFileData(self.target, data)
|
||||||
|
|
||||||
|
def test_01_import_with_size(self):
|
||||||
|
data = b'abcd' * 1024
|
||||||
|
size = len(data)
|
||||||
|
self.setup_import(size)
|
||||||
|
|
||||||
|
self.run_rpc('admin.vm.volume.ImportWithSize', 'volume',
|
||||||
|
str(size).encode() + b'\n' + data)
|
||||||
|
|
||||||
|
self.assertMethodCalled('internal.vm.volume.ImportBegin', 'volume',
|
||||||
|
str(size).encode())
|
||||||
|
self.assertMethodCalled('internal.vm.volume.ImportEnd', 'volume',
|
||||||
|
b'ok')
|
||||||
|
self.assertFileData(self.target, data)
|
||||||
|
|
||||||
|
def test_02_import_not_enough_data(self):
|
||||||
|
data = b'abcd' * 1024
|
||||||
|
size = len(data) + 1
|
||||||
|
self.setup_import(size)
|
||||||
|
|
||||||
|
self.run_rpc('admin.vm.volume.Import', 'volume', data)
|
||||||
|
|
||||||
|
self.assertMethodCalled('internal.vm.volume.ImportBegin', 'volume')
|
||||||
|
self.assertMethodCalled(
|
||||||
|
'internal.vm.volume.ImportEnd', 'volume',
|
||||||
|
b'fail\n' +
|
||||||
|
('not enough data (copied {} bytes, expected {} bytes)'
|
||||||
|
.format(len(data), size).encode()))
|
||||||
|
|
||||||
|
def test_03_import_too_much_data(self):
|
||||||
|
data = b'abcd' * 1024
|
||||||
|
size = len(data) - 1
|
||||||
|
self.setup_import(size)
|
||||||
|
|
||||||
|
output = self.run_rpc('admin.vm.volume.Import', 'volume', data)
|
||||||
|
|
||||||
|
self.assertMethodCalled('internal.vm.volume.ImportBegin', 'volume')
|
||||||
|
self.assertMethodCalled(
|
||||||
|
'internal.vm.volume.ImportEnd', 'volume',
|
||||||
|
b'fail\n' +
|
||||||
|
('too much data (expected {} bytes)'
|
||||||
|
.format(size).encode()))
|
@ -300,6 +300,7 @@ fi
|
|||||||
%{python3_sitelib}/qubes/tests/ext.py
|
%{python3_sitelib}/qubes/tests/ext.py
|
||||||
%{python3_sitelib}/qubes/tests/firewall.py
|
%{python3_sitelib}/qubes/tests/firewall.py
|
||||||
%{python3_sitelib}/qubes/tests/init.py
|
%{python3_sitelib}/qubes/tests/init.py
|
||||||
|
%{python3_sitelib}/qubes/tests/rpc_import.py
|
||||||
%{python3_sitelib}/qubes/tests/storage.py
|
%{python3_sitelib}/qubes/tests/storage.py
|
||||||
%{python3_sitelib}/qubes/tests/storage_file.py
|
%{python3_sitelib}/qubes/tests/storage_file.py
|
||||||
%{python3_sitelib}/qubes/tests/storage_reflink.py
|
%{python3_sitelib}/qubes/tests/storage_reflink.py
|
||||||
|
Loading…
Reference in New Issue
Block a user