Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ checks:tests:
# itself unless there's memory pressure, and the system will fail to request
# memory from qmemman since qmemman will not see enough memory to run.
sudo modprobe zfs zfs_arc_max=67108864
- git clone https://github.com/QubesOS/qubes-linux-utils ~/qubes-linux-utils
# the below 2 lines work like a chisel and hammer in a caveman's hand :/
- make -C ~/qubes-linux-utils/qrexec-lib NO_REBUILD_TABLE=1
- sudo install ~/qubes-linux-utils/qrexec-lib/libqubes-pure.so.0 /usr/lib64
script:
- PYTHONPATH=test-packages:~/qubes-core-qrexec ./run-tests
stage: checks
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ ADMIN_API_METHODS_SIMPLE = \
admin.vm.firewall.GetPolicy \
admin.vm.firewall.SetPolicy \
admin.vm.firewall.Reload \
admin.vm.notes.Get \
admin.vm.notes.Set \
admin.vm.property.Get \
admin.vm.property.GetAll \
admin.vm.property.GetDefault \
Expand Down
52 changes: 52 additions & 0 deletions qubes/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import subprocess
import pathlib

from ctypes import CDLL

import libvirt
import lxml.etree
import importlib.metadata
Expand All @@ -53,6 +55,9 @@
DeviceInterface,
)

# To validate & sanitise UTF8 strings
LIBQUBES_PURE = "libqubes-pure.so.0"


class QubesMgmtEventsDispatcher:
def __init__(self, filters, send_event):
Expand Down Expand Up @@ -2036,3 +2041,50 @@ async def vm_current_state(self):
"power_state": self.dest.get_power_state(),
}
return " ".join("{}={}".format(k, v) for k, v in state.items())

@qubes.api.method(
"admin.vm.notes.Get", no_payload=True, scope="local", read=True
)
async def vm_notes_get(self):
"""Get qube notes"""
self.enforce(self.dest.name != "dom0")
self.fire_event_for_permission()
notes = self.dest.get_notes()
return notes

@qubes.api.method("admin.vm.notes.Set", scope="local", write=True)
async def vm_notes_set(self, untrusted_payload):
"""Set qube notes"""
self.enforce(self.dest.name != "dom0")
self.fire_event_for_permission()
if len(untrusted_payload) > 256000:
raise qubes.exc.ProtocolError(
"Maximum note size is 256000 bytes ({} bytes received)".format(
len(untrusted_payload)
)
)

# Sanitise the incoming utf8 notes with libqubes-pure
try:
libqubespure = CDLL(LIBQUBES_PURE)
notes = "".join(
[
(
c
# first we check with our advanced unicode sanitisation
if libqubespure.qubes_pure_code_point_safe_for_display(
ord(c)
)
# validate tab and newline since qubespure excludes them
or c in "\t\n"
else "_"
)
for c in untrusted_payload.decode("utf8")
]
)
except Exception as e:
Comment thread
alimirjamali marked this conversation as resolved.
raise qubes.exc.ProtocolError(
"Unable to sanitise qube notes: " + str(e)
)

self.dest.set_notes(notes)
4 changes: 4 additions & 0 deletions qubes/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@
if os.path.exists(firewall_conf):
vm_files.append(self.FileToBackup(firewall_conf, subdir))

notes_file_path = os.path.join(vm.dir_path, vm.notes_file)
if os.path.exists(notes_file_path):
vm_files.append(self.FileToBackup(notes_file_path, subdir))

Check warning on line 438 in qubes/backup.py

View check run for this annotation

Codecov / codecov/patch

qubes/backup.py#L438

Added line #L438 was not covered by tests

if not vm_files:
# subdir/ is needed in the tar file, otherwise restore
# of a (Disp)VM without any backed up files is going
Expand Down
71 changes: 71 additions & 0 deletions qubes/tests/api_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,77 @@ def test_450_property_reset(self):
self.assertIsNone(value)
self.app.save.assert_called_once_with()

def test_notes_get(self):
notes = "For Your Eyes Only"
self.app.domains["test-vm1"].get_notes = unittest.mock.Mock()
self.app.domains["test-vm1"].get_notes.configure_mock(
**{"return_value": notes}
)
value = self.call_mgmt_func(b"admin.vm.notes.Get", b"test-vm1")
self.assertEqual(value, notes)
self.app.domains["test-vm1"].get_notes.configure_mock(
**{"side_effect": qubes.exc.QubesException()}
)
with self.assertRaises(qubes.exc.QubesException):
self.call_mgmt_func(b"admin.vm.notes.Get", b"test-vm1")
self.assertEqual(
self.app.domains["test-vm1"].get_notes.mock_calls,
[unittest.mock.call(), unittest.mock.call()],
)
self.assertFalse(self.app.save.called)

def test_notes_set(self):
self.app.domains["test-vm1"].set_notes = unittest.mock.Mock()

# Acceptable note
payload = b"For Your Eyes Only"
self.call_mgmt_func(
b"admin.vm.notes.Set",
b"test-vm1",
payload=payload,
)
self.app.domains["test-vm1"].set_notes.assert_called_with(
payload.decode()
)

# Note with new-line & tab characters
payload = b"def python_example_function():\n\tpass"
self.call_mgmt_func(
b"admin.vm.notes.Set",
b"test-vm1",
payload=payload,
)
self.app.domains["test-vm1"].set_notes.assert_called_with(
payload.decode()
)

# Note with un-acceptable character (backspace, non-breaking space, ...)
payload = "\b\xa0\u200b\u200c\u200d".encode()
self.call_mgmt_func(
b"admin.vm.notes.Set",
b"test-vm1",
payload=payload,
)
self.app.domains["test-vm1"].set_notes.assert_called_with("_____")

# Invalid UTF8 sequence
with self.assertRaises(qubes.exc.ProtocolError):
payload = b"\xd8"
self.call_mgmt_func(
b"admin.vm.notes.Set",
b"test-vm1",
payload=payload,
)

# Unacceptable oversized note
with self.assertRaises(qubes.exc.ProtocolError):
payload = ("x" * 256001).encode()
self.call_mgmt_func(
b"admin.vm.notes.Set",
b"test-vm1",
payload=payload,
)

def device_list_testclass(self, vm, event):
if vm is not self.vm:
return
Expand Down
18 changes: 18 additions & 0 deletions qubes/tests/vm/qubesvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3096,6 +3096,24 @@ def test_801_ordering(self):
self.app, None, qid=1, name="bogus"
) > qubes.vm.adminvm.AdminVM(self.app, None)

def test_802_notes(self):
vm = self.get_vm()
notes = "For Your Eyes Only"
with unittest.mock.patch(
"builtins.open", unittest.mock.mock_open(read_data=notes)
) as mock_open:
with self.assertNotRaises(qubes.exc.QubesException):
vm.set_notes(notes)
self.assertEqual(vm.get_notes(), notes)
mock_open.side_effect = FileNotFoundError()
self.assertEqual(vm.get_notes(), "")
with self.assertRaises(qubes.exc.QubesException):
mock_open.side_effect = PermissionError()
vm.set_notes(notes)
with self.assertRaises(qubes.exc.QubesException):
mock_open.side_effect = PermissionError()
vm.get_notes()

def test_810_bootmode_kernelopts(self):
vm = self.get_vm(cls=qubes.vm.appvm.AppVM)
vm.template = self.get_vm(cls=qubes.vm.templatevm.TemplateVM)
Expand Down
37 changes: 37 additions & 0 deletions qubes/vm/qubesvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2677,6 +2677,43 @@

return result

#
# free-form text for descriptions, notes, comments, remarks, etc.
#

@property
def notes_file(self) -> str:
"""Notes file name within /var/lib/qubes (per each qube sub-dir)"""
return "notes.txt"

def get_notes(self) -> str:
"""Read the notes file and return its content"""
try:
with open(
os.path.join(self.dir_path, self.notes_file), encoding="utf8"
) as fd:
return fd.read()
except FileNotFoundError:
return ""
except Exception as exc:
raise qubes.exc.QubesException(
"Failed to read notes file: " + str(exc)
)

def set_notes(self, notes: str):
"""Write to notes file. Return True on success, False on error"""
try:
with open(
os.path.join(self.dir_path, self.notes_file),
"w",
encoding="utf8",
) as fd:
fd.write(notes)
except Exception as exc:
raise qubes.exc.QubesException(
"Failed to write notes file: " + str(exc)
)

Check warning on line 2715 in qubes/vm/qubesvm.py

View check run for this annotation

Codecov / codecov/patch

qubes/vm/qubesvm.py#L2715

Added line #L2715 was not covered by tests

#
# helper methods
#
Expand Down
5 changes: 5 additions & 0 deletions rpm_spec/core-dom0.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ Conflicts: qubes-audio-dom0 < 4.3.5
# Required for qvm-console* tools
Requires: socat

# Requires libqubes-pure for qube notes utf8 sanitisation
Requires: qubes-utils-libs

%{?systemd_requires}

Obsoletes: qubes-core-dom0-doc <= 4.0
Expand Down Expand Up @@ -286,6 +289,8 @@ admin.vm.feature.Set
admin.vm.firewall.Get
admin.vm.firewall.Reload
admin.vm.firewall.Set
admin.vm.notes.Get
admin.vm.notes.Set
admin.vm.property.Get
admin.vm.property.GetAll
admin.vm.property.GetDefault
Expand Down