New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
overlord/ifacestate: setup security backends phased by backends first #6128
Changes from all commits
61727a4
b59f5a4
6127cb0
dd08e0e
9588c9f
bd190c2
071616a
b6b1626
71b219f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -310,6 +310,26 @@ func (m *InterfaceManager) reloadConnections(snapName string) ([]string, error) | |
return result, nil | ||
} | ||
|
||
func (m *InterfaceManager) setupSecurityByBackend(task *state.Task, snaps []*snap.Info, opts []interfaces.ConfinementOptions) error { | ||
st := task.State() | ||
|
||
// Setup all affected snaps, start with the most important security | ||
// backend and run it for all snaps. See LP: 1802581 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth clarifying the comment and say the ordering is established at init time (and maintained in the repo). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed, this comment probably needs some expanding, important doesn't convey the fact that some backend behavior depends on the work of some other backend |
||
for _, backend := range m.repo.Backends() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add some kind of unit tests that detects somehow the change of order? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I will work on that now. |
||
for i, snapInfo := range snaps { | ||
st.Unlock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the reason is done like that is that the systemd backend can take a long time |
||
err := backend.Setup(snapInfo, opts[i], m.repo) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting observation. I think we need to sit down and design the backend setup process to optimise the apparmor wasted cycles we spend. In my mind I was thinking about backend.StartTransaction() ... and CommitTransaction() like API where we don't commit unless something needs to observe the intermediate state of some interfaces being connected and setup and some not yet when we know there are more setup calls just around the corner. |
||
st.Lock() | ||
if err != nil { | ||
task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapInfo.InstanceName(), err) | ||
return err | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (m *InterfaceManager) setupSnapSecurity(task *state.Task, snapInfo *snap.Info, opts interfaces.ConfinementOptions) error { | ||
st := task.State() | ||
instanceName := snapInfo.InstanceName() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
#!/usr/bin/python3 | ||
"""Mock the /sys/class/gpio subsystem""" | ||
|
||
import atexit | ||
import logging | ||
import os | ||
import selectors | ||
import shutil | ||
import subprocess | ||
import sys | ||
import tempfile | ||
|
||
from typing import Type | ||
|
||
|
||
def read_gpio_pin(read_fd: int) -> str: | ||
""" | ||
read_gpio_pin reads from the given fd and return a string with the | ||
numeric pin or "" if an invalid pin was specified. | ||
""" | ||
data = os.read(read_fd, 128) | ||
pin = data.decode().strip() | ||
if not pin.isnumeric(): | ||
logging.warning("invalid gpio pin %s", pin) | ||
return "" | ||
return "gpio{}".format(pin) | ||
|
||
|
||
def export_ready(read_fd: int, _) -> bool: | ||
"""export_ready is run when the "export" file is ready for reading""" | ||
pin = read_gpio_pin(read_fd) | ||
# allow quit | ||
if pin == 'quit': | ||
return False | ||
if pin: | ||
with open(pin, "w"): | ||
pass | ||
return True | ||
|
||
|
||
def unexport_ready(read_fd: int, _) -> bool: | ||
"""export_ready is run when the "unexport" file is ready for reading""" | ||
pin = read_gpio_pin(read_fd) | ||
try: | ||
os.remove(pin) | ||
except OSError as err: | ||
logging.warning("got exception %s", err) | ||
return True | ||
|
||
|
||
def dispatch(sel): | ||
""" | ||
dispatch dispatches the events from the "sel" source | ||
""" | ||
for key, mask in sel.select(): | ||
callback = key.data | ||
if not callback(key.fileobj, mask): | ||
return False | ||
return True | ||
|
||
|
||
def main(): | ||
"""the main method""" | ||
if os.getuid() != 0: | ||
print("must run as root") | ||
sys.exit(1) | ||
|
||
# setup mock env | ||
mock_gpio_dir = tempfile.mkdtemp("mock-gpio") | ||
atexit.register(shutil.rmtree, mock_gpio_dir) | ||
os.chdir(mock_gpio_dir) | ||
subprocess.check_call( | ||
["mount", "--bind", mock_gpio_dir, "/sys/class/gpio"]) | ||
atexit.register(lambda: subprocess.call(["umount", "/sys/class/gpio"])) | ||
|
||
# fake gpio export/unexport files | ||
os.mkfifo("export") | ||
os.mkfifo("unexport") | ||
|
||
# react to the export/unexport calls | ||
sel = selectors.DefaultSelector() | ||
efd = os.open("export", os.O_RDWR | os.O_NONBLOCK) | ||
ufd = os.open("unexport", os.O_RDWR | os.O_NONBLOCK) | ||
sel.register(efd, selectors.EVENT_READ, export_ready) | ||
sel.register(ufd, selectors.EVENT_READ, unexport_ready) | ||
while True: | ||
if not dispatch(sel): | ||
break | ||
|
||
# cleanup when we get a quit call | ||
os.close(efd) | ||
os.close(ufd) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
summary: Regression test for LP 1802581 | ||
|
||
description: | | ||
When using the snapd GPIO interface the slot side needs to export the GPIO | ||
pint so that a file appears under /sys/class/gpio/gpioNNN. That file is a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "pint" 🍺 |
||
symbolic link to a special, platform specific device path. The symbolic | ||
link needs to be evaluated so that apparmor rules for the plug side can be | ||
constructed. | ||
|
||
A customer has reported that when a snap is disabled and then re-enabled | ||
the GPIO pin is not exported and apparmor backend cannot setup security | ||
of the snap being enabled. | ||
|
||
A similar issue has occurred once in the past where the order in which | ||
security backends operated was not deterministic. We fixed the issue by | ||
applying a fixed order so that the systemd backend, responsible for | ||
exporting the pin, would always run before the apparmor backend, which | ||
could now rely on the pin being exposed to userspace. | ||
|
||
systems: [ubuntu-core-16-64] | ||
|
||
prepare: | | ||
echo "Run mock gpio daemon" | ||
systemd-run --unit mock-gpio -- "$(pwd)/mock-gpio.py" | ||
# shellcheck source=tests/lib/systemd.sh | ||
. "$TESTSLIB/systemd.sh" | ||
wait_for_service mock-gpio | ||
|
||
restore: | | ||
systemctl stop mock-gpio || true | ||
# for good measure, mock-gpio.py does this umount already on exit | ||
umount /sys/class/gpio || true | ||
|
||
debug: | | ||
journalctl -u mock-gpio.service | ||
|
||
execute: | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice test! |
||
echo "Install a snap that uses the gpio consumer" | ||
#shellcheck source=tests/lib/snaps.sh | ||
. "$TESTSLIB"/snaps.sh | ||
install_local gpio-consumer | ||
|
||
echo "And connect the gpio pin" | ||
snap connect gpio-consumer:gpio :gpio-pin | ||
snap interfaces | MATCH ":gpio-pin.*gpio-consumer:gpio" | ||
|
||
# LP-1802581 | ||
echo "Now disable and enable the snap to ensure lp: #1802581 is fixed" | ||
snap disable gpio-consumer | ||
snap enable gpio-consumer | ||
|
||
echo "Check that the connection is still here after enable" | ||
snap interfaces | MATCH ":gpio-pin.*gpio-consumer:gpio" | ||
|
||
echo "Ensure that our mock service is full functional" | ||
systemctl status mock-gpio.service | MATCH "Active: active" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could do this before the for loop instead of after
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not because Sort but then maybe the comment needs moving