Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5bcef5d
core-initrd: update changelog with latest PPA upload
alfonsosanchezbeato Apr 16, 2026
2da6fd0
o/confdbstate: check for ephemeral change when missing save-view hook…
miguelpires Apr 20, 2026
017bd1b
tests: source nested.sh before calling function in prepare.sh (#16937)
maykathm Apr 20, 2026
538fb99
tests/lib/nested.sh: ensure test tools that need python can run
alfonsosanchezbeato Apr 20, 2026
970e0b6
tests: use noble for lp-1871652 (#16938)
maykathm Apr 21, 2026
604f5e7
github: use symlink for debian folder (#16948)
maykathm Apr 21, 2026
3fbb81c
tests: fix upgrade-from-release
miguelpires Apr 21, 2026
582e121
interfaces/builtin/content: add unit tests for non-obvious targets (#…
bboozzoo Apr 21, 2026
19a413d
o/confdbstate: block concurrent snapctl accesses (#16834)
miguelpires Apr 21, 2026
7573c01
o/h/ctlcmd, t/main: add per-change rate limit, unit tests, spread tes…
Rnfudge02 Apr 21, 2026
e71b916
fix OOB read in C mountinfo parser and add regression tests
Copilot Apr 13, 2026
57533c0
cmd/libsnap-confine-private: add lone trailing backslash case to part…
Copilot Apr 13, 2026
4a5044e
cmd/libsnap-confine-private: clang-format mountinfo.c and mountinfo-t…
Copilot Apr 13, 2026
9401825
cmd: tweak spaces
zyga Apr 14, 2026
1c95eb2
tests: set path in enviornment for service in enable-disable-units-gpio
maykathm Apr 22, 2026
d8ede65
tests/main/disk-space-awareness: ensure consistent state before and a…
bboozzoo Apr 22, 2026
74ab8c6
overlord, snap: omit snapd refresh suggestion for ISA-related assume …
alfonsosanchezbeato Apr 22, 2026
6a01a45
tests: enable more nested tests on 26 (#16939)
valentindavid Apr 22, 2026
24531bf
seclog: add structured security logger - at this point both journal a…
ernestl Oct 7, 2025
62793aa
many: remove journal sink
ernestl Apr 22, 2026
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: 2 additions & 2 deletions .github/workflows/deb-builds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ jobs:
target_system="${{ inputs.os }}-${{ inputs.os-version }}"
case "$target_system" in
debian-sid)
cp -av packaging/debian-sid debian
ln -sfn packaging/debian-sid debian
;;
ubuntu-*)
cp -av packaging/ubuntu-16.04 debian
ln -sfn packaging/ubuntu-16.04 debian
;;
*)
echo "unsupported deb packaging for $target_system"
Expand Down
51 changes: 51 additions & 0 deletions cmd/libsnap-confine-private/mountinfo-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,55 @@ static void test_parse_mountinfo_entry__broken_octal_escaping(void) {
g_assert_null(entry->next);
}

static void test_parse_mountinfo_entry__partial_escape_oob(void) {
// Regression tests: partial octal escape sequences (fewer than 3 octal
// digits after the backslash, or just a trailing backslash) must not cause
// out-of-bounds reads. Each partial escape is copied verbatim.
const char *line;
struct sc_mountinfo_entry *entry;

// Lone trailing backslash at end of string.
line = "2074 27 0:54 / /tmp/dir rw - tmpfs source rw\\";
entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify)sc_free_mountinfo_entry, entry);
g_assert_cmpstr(entry->mount_source, ==, "source");
g_assert_cmpstr(entry->super_opts, ==, "rw\\");

// Backslash followed by one octal digit at end of string.
line = "2074 27 0:54 / /tmp/dir rw - tmpfs source rw\\0";
entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify)sc_free_mountinfo_entry, entry);
g_assert_cmpstr(entry->mount_source, ==, "source");
g_assert_cmpstr(entry->super_opts, ==, "rw\\0");

// Backslash followed by two octal digits at end of string.
line = "2074 27 0:54 / /tmp/dir rw - tmpfs source rw\\05";
entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify)sc_free_mountinfo_entry, entry);
g_assert_cmpstr(entry->mount_source, ==, "source");
g_assert_cmpstr(entry->super_opts, ==, "rw\\05");

// Backslash followed by one octal digit then space (partial escape at
// end of a space-delimited field, not at end of string).
line = "2074 27 0:54 / /tmp/dir rw - tmpfs source\\5 rw";
entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify)sc_free_mountinfo_entry, entry);
g_assert_cmpstr(entry->mount_source, ==, "source\\5");
g_assert_cmpstr(entry->super_opts, ==, "rw");

// Backslash followed by two octal digits then space.
line = "2074 27 0:54 / /tmp/dir rw - tmpfs source\\57 rw";
entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify)sc_free_mountinfo_entry, entry);
g_assert_cmpstr(entry->mount_source, ==, "source\\57");
g_assert_cmpstr(entry->super_opts, ==, "rw");
}

static void test_parse_mountinfo_entry__unescaped_whitespace(void) {
// The kernel does not escape '\r'
const char *line = "2074 27 0:54 / /tmp/strange\rdir rw,relatime shared:1039 - tmpfs tmpfs rw";
Expand Down Expand Up @@ -271,6 +320,8 @@ static void __attribute__((constructor)) init(void) {
g_test_add_func("/mountinfo/parse_mountinfo_entry/octal_escaping", test_parse_mountinfo_entry__octal_escaping);
g_test_add_func("/mountinfo/parse_mountinfo_entry/broken_octal_escaping",
test_parse_mountinfo_entry__broken_octal_escaping);
g_test_add_func("/mountinfo/parse_mountinfo_entry/partial_escape_oob",
test_parse_mountinfo_entry__partial_escape_oob);
g_test_add_func("/mountinfo/parse_mountinfo_entry/unescaped_whitespace",
test_parse_mountinfo_entry__unescaped_whitespace);
g_test_add_func("/mountinfo/parse_mountinfo_entry/broken_9p_superblock",
Expand Down
18 changes: 9 additions & 9 deletions cmd/libsnap-confine-private/mountinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ static char *parse_next_string_field_ex(sc_mountinfo_entry *entry, const char *l
bool allow_spaces_in_field) {
const char *input = &line[*offset];
char *output = &entry->line_buf[*offset];
size_t input_idx = 0; // reading index
size_t output_idx = 0; // writing index
size_t input_idx = 0; // reading index
size_t output_idx = 0; // writing index
size_t input_len = strlen(input); // length of remaining input (used for bounds checks below)

// Scan characters until we run out of memory to scan or we find a
// space. The kernel uses simple octal escape sequences for the
Expand Down Expand Up @@ -169,14 +170,13 @@ static char *parse_next_string_field_ex(sc_mountinfo_entry *entry, const char *l
break;
} else if (c == '\\') {
// Three *more* octal digits required for the escape
// sequence. For reference see mangle_path() in
// fs/seq_file.c. Note that is_octal_digit returns
// false on the string terminator character NUL and the
// short-circuiting behavior of && makes this check
// correct even if '\\' is the last character of the
// string.
// sequence. For reference see mangle_path() in
// fs/seq_file.c. We explicitly verify that at least 3
// more bytes remain before the end of the string to
// prevent out-of-bounds reads when the input contains
// fewer than 3 bytes after the backslash.
const char *s = &input[input_idx];
if (is_octal_digit(s[1]) && is_octal_digit(s[2]) && is_octal_digit(s[3])) {
if (input_idx + 4 <= input_len && is_octal_digit(s[1]) && is_octal_digit(s[2]) && is_octal_digit(s[3])) {
// Unescape the octal value encoded in s[1],
// s[2] and s[3]. Because we are working with
// byte values there are no issues related to
Expand Down
22 changes: 21 additions & 1 deletion cmd/snapd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ package main

import (
"time"

"github.com/snapcore/snapd/seclog"
)

var (
Run = run
Run = run
SetupSecurityLogger = setupSecurityLogger
DisableSecurityLogger = disableSecurityLogger
)

func MockSyscheckCheckSystem(f func() error) (restore func()) {
Expand All @@ -35,6 +39,22 @@ func MockSyscheckCheckSystem(f func() error) (restore func()) {
}
}

func MockSeclogSetup(f func(seclog.Impl, seclog.Sink, string, seclog.Level) error) (restore func()) {
old := seclogSetup
seclogSetup = f
return func() {
seclogSetup = old
}
}

func MockSeclogDisable(f func() error) (restore func()) {
old := seclogDisable
seclogDisable = f
return func() {
seclogDisable = old
}
}

func MockCheckRunningConditionsRetryDelay(d time.Duration) (restore func()) {
oldCheckRunningConditionsRetryDelay := checkRunningConditionsRetryDelay
checkRunningConditionsRetryDelay = d
Expand Down
21 changes: 21 additions & 0 deletions cmd/snapd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/sandbox"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/seclog"
"github.com/snapcore/snapd/snapdenv"
"github.com/snapcore/snapd/snapdtool"
"github.com/snapcore/snapd/syscheck"
Expand All @@ -41,13 +42,33 @@ import (

var (
syscheckCheckSystem = syscheck.CheckSystem
seclogSetup = seclog.Setup
seclogDisable = seclog.Disable
)

const secLogAppID = "canonical.snapd.snapd"
const secLogMinLevel seclog.Level = seclog.LevelInfo

func setupSecurityLogger() {
if err := seclogSetup(seclog.ImplSlog, seclog.SinkAudit, secLogAppID, secLogMinLevel); err != nil {
logger.Noticef("WARNING: %v", err)
}
}

func disableSecurityLogger() {
if err := seclogDisable(); err != nil {
logger.Noticef("WARNING: cannot disable security logger: %v", err)
}
}

func init() {
logger.SimpleSetup(nil)
setupSecurityLogger()
}

func main() {
defer disableSecurityLogger()

// When preseeding re-exec is not used
if snapdenv.Preseeding() {
logger.Noticef("running for preseeding")
Expand Down
43 changes: 43 additions & 0 deletions cmd/snapd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/seclog"
"github.com/snapcore/snapd/testutil"
)

Expand All @@ -60,6 +61,48 @@ func (s *snapdSuite) SetUpTest(c *C) {
s.AddCleanup(restore)
}

func (s *snapdSuite) TestSetupSecurityLoggerWarnsOnError(c *C) {
logbuf, restore := logger.MockLogger()
defer restore()

restore = snapd.MockSeclogSetup(func(impl seclog.Impl, sink seclog.Sink, appID string, level seclog.Level) error {
return fmt.Errorf("security logger disabled: cannot open audit socket: permission denied")
})
defer restore()

snapd.SetupSecurityLogger()

c.Check(logbuf.String(), testutil.Contains, "WARNING: security logger disabled: cannot open audit socket: permission denied")
}

func (s *snapdSuite) TestDisableSecurityLoggerCallsDisable(c *C) {
_, restore := logger.MockLogger()
defer restore()

disabled := false
restore = snapd.MockSeclogDisable(func() error {
disabled = true
return nil
})
defer restore()

snapd.DisableSecurityLogger()
c.Check(disabled, Equals, true)
}

func (s *snapdSuite) TestDisableSecurityLoggerWarnsOnError(c *C) {
logbuf, restore := logger.MockLogger()
defer restore()

restore = snapd.MockSeclogDisable(func() error {
return fmt.Errorf("audit socket busy")
})
defer restore()

snapd.DisableSecurityLogger()
c.Check(logbuf.String(), testutil.Contains, "WARNING: cannot disable security logger: audit socket busy")
}

func (s *snapdSuite) TestSyscheckFailGoesIntoDegradedMode(c *C) {
logbuf, restore := logger.MockLogger()
defer restore()
Expand Down
6 changes: 6 additions & 0 deletions core-initrd/24.04/debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
ubuntu-core-initramfs (69+2.75.2+g199.9a8c2f3+24.04) noble; urgency=medium

* Update to snapd version 2.75.2+g199.9a8c2f3

-- Alfonso Sanchez-Beato <alfonso.sanchez-beato@canonical.com> Wed, 15 Apr 2026 16:49:42 -0400

ubuntu-core-initramfs (69+2.75+g75.4b39daa+24.04) noble; urgency=medium

* Update to snapd version 2.75+g75.4b39daa
Expand Down
6 changes: 6 additions & 0 deletions core-initrd/25.10/debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
ubuntu-core-initramfs (72+2.75.2+g199.9a8c2f3+25.10) questing; urgency=medium

* Update to snapd version 2.75.2+g199.9a8c2f3

-- Alfonso Sanchez-Beato <alfonso.sanchez-beato@canonical.com> Wed, 15 Apr 2026 16:50:12 -0400

ubuntu-core-initramfs (72+2.75+g75.4b39daa+25.10) questing; urgency=medium

* Update to snapd version 2.75+g75.4b39daa
Expand Down
6 changes: 6 additions & 0 deletions core-initrd/26.04/debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
ubuntu-core-initramfs (73+2.75.2+g199.9a8c2f3+26.04) resolute; urgency=medium

* Update to snapd version 2.75.2+g199.9a8c2f3

-- Alfonso Sanchez-Beato <alfonso.sanchez-beato@canonical.com> Wed, 15 Apr 2026 16:50:37 -0400

ubuntu-core-initramfs (73+2.75+g75.4b39daa+26.04) resolute; urgency=medium

* Update to snapd version 2.75+g75.4b39daa
Expand Down
49 changes: 39 additions & 10 deletions daemon/api_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/seclog"
"github.com/snapcore/snapd/store"
)

Expand Down Expand Up @@ -68,6 +69,9 @@ var (
deviceStateCreateUser = devicestate.CreateUser
deviceStateCreateKnownUsers = devicestate.CreateKnownUsers
deviceStateRemoveUser = devicestate.RemoveUser

seclogLogLoginSuccess = seclog.LogLoginSuccess
seclogLogLoginFailure = seclog.LogLoginFailure
)

// userResponseData contains the data releated to user creation/login/query
Expand All @@ -83,6 +87,17 @@ type userResponseData struct {

var isEmailish = regexp.MustCompile(`.@.*\..`).MatchString

// loginError logs a login failure to the security audit log and returns resp
// unchanged. It is a convenience wrapper so that each error return path in
// loginUser can log with a single call.
func loginError(resp *apiError, snapdUser seclog.SnapdUser, code string) *apiError {
seclogLogLoginFailure(snapdUser, seclog.Reason{
Code: code,
Message: resp.Message,
})
return resp
}

func loginUser(c *Command, r *http.Request, user *auth.UserState) Response {
var loginData struct {
Username string `json:"username"`
Expand Down Expand Up @@ -116,41 +131,49 @@ func loginUser(c *Command, r *http.Request, user *auth.UserState) Response {
}
}

// Build the user identity for security audit logging. At this
// point we know the email and optional username; the numeric ID
// is only available after successful authentication.
snapdUser := seclog.SnapdUser{
SystemUserName: loginData.Username,
StoreUserEmail: loginData.Email,
}

overlord := c.d.overlord
st := overlord.State()
theStore := storeFrom(c.d)
macaroon, discharge, err := theStore.LoginUser(loginData.Email, loginData.Password, loginData.Otp)
switch err {
case store.ErrAuthenticationNeeds2fa:
return &apiError{
return loginError(&apiError{
Status: 401,
Message: err.Error(),
Kind: client.ErrorKindTwoFactorRequired,
}
}, snapdUser, seclog.ReasonTwoFactorRequired)
case store.Err2faFailed:
return &apiError{
return loginError(&apiError{
Status: 401,
Message: err.Error(),
Kind: client.ErrorKindTwoFactorFailed,
}
}, snapdUser, seclog.ReasonTwoFactorFailed)
default:
switch err := err.(type) {
case store.InvalidAuthDataError:
return &apiError{
return loginError(&apiError{
Status: 400,
Message: err.Error(),
Kind: client.ErrorKindInvalidAuthData,
Value: err,
}
}, snapdUser, seclog.ReasonInvalidAuthData)
case store.PasswordPolicyError:
return &apiError{
return loginError(&apiError{
Status: 401,
Message: err.Error(),
Kind: client.ErrorKindPasswordPolicy,
Value: err,
}
}, snapdUser, seclog.ReasonPasswordPolicy)
}
return Unauthorized(err.Error())
return loginError(Unauthorized(err.Error()), snapdUser, seclog.ReasonInvalidCredentials)
case nil:
// continue
}
Expand All @@ -172,9 +195,15 @@ func loginUser(c *Command, r *http.Request, user *auth.UserState) Response {
}
st.Unlock()
if err != nil {
return InternalError("cannot persist authentication details: %v", err)
return loginError(InternalError("cannot persist authentication details: %v", err), snapdUser, seclog.ReasonInternal)
}

snapdUser.ID = int64(user.ID)
snapdUser.SystemUserName = user.Username
snapdUser.StoreUserEmail = user.Email
snapdUser.Expiration = user.Expiration
seclogLogLoginSuccess(snapdUser)

result := userResponseData{
ID: user.ID,
Username: user.Username,
Expand Down
Loading
Loading