Skip to content

Commit 10d529a

Browse files
authored
seclog: add slog implementation and audit sink (part 2) (#16988)
* seclog: add slog implementation and audit sink * seclog: review improvements * seclog: review improvements * seclog: rename LogAny to LogEvent * seclog: review improvements * seclog: renamed syscall ops interface * seclog: fix kernel null termination and improve alignment and seq number * seclog: avoid using sync/atomic.Uint32 - not available in go 1.18 * seclog: avoid using slices * seclog: fixed grammer error * seclog: do not log security logger enabled/disabled when nop logger is used * seclog: track audit fd open state to prevent accidental close of fd 0. * seclog: moved slog logValue to end of file
1 parent 6ef8a8e commit 10d529a

11 files changed

Lines changed: 1055 additions & 17 deletions

File tree

seclog/audit_linux.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// -*- Mode: Go; indent-tabs-mode: t -*-
2+
3+
/*
4+
* Copyright (C) 2026 Canonical Ltd
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3 as
8+
* published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*
18+
*/
19+
20+
package seclog
21+
22+
import (
23+
"fmt"
24+
"sync/atomic"
25+
"syscall"
26+
27+
"github.com/snapcore/snapd/arch"
28+
)
29+
30+
const (
31+
// AUDIT_TRUSTED_APP is the audit message type for trusted application messages.
32+
// See https://github.com/linux-audit/audit-userspace/blob/a54613b2b6233669972d55f1f5463ae4757700be/lib/audit-records.h#L75
33+
auditTrustedApp = 1121
34+
)
35+
36+
// syscallOps abstracts the syscall operations needed to open,
37+
// send to, and close a netlink socket. Production code uses [realSyscallOps];
38+
// tests can substitute a recording or stubbing implementation.
39+
type syscallOps interface {
40+
Socket(domain, typ, proto int) (int, error)
41+
Sendto(fd int, payload []byte, flags int, to syscall.Sockaddr) error
42+
Close(fd int) error
43+
}
44+
45+
// realSyscallOps delegates every operation to the corresponding syscall.
46+
type realSyscallOps struct{}
47+
48+
func (realSyscallOps) Socket(domain, typ, proto int) (int, error) {
49+
return syscall.Socket(domain, typ, proto)
50+
}
51+
52+
func (realSyscallOps) Sendto(fd int, payload []byte, flags int, to syscall.Sockaddr) error {
53+
return syscall.Sendto(fd, payload, flags, to)
54+
}
55+
56+
func (realSyscallOps) Close(fd int) error {
57+
return syscall.Close(fd)
58+
}
59+
60+
var sys syscallOps = realSyscallOps{}
61+
62+
// AuditWriter implements [io.WriteCloser].
63+
// It must be created via [OpenAuditWriter]; the zero value is not usable.
64+
type AuditWriter struct {
65+
fd int
66+
seq uint32
67+
opened bool
68+
}
69+
70+
// OpenAuditWriter opens a netlink audit socket and returns an [AuditWriter]
71+
// that sends each written payload as an AUDIT_TRUSTED_APP.
72+
func OpenAuditWriter() (*AuditWriter, error) {
73+
// SOCK_CLOEXEC prevents the fd from leaking to child processes.
74+
fd, err := sys.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW|syscall.SOCK_CLOEXEC, syscall.NETLINK_AUDIT)
75+
if err != nil {
76+
return nil, fmt.Errorf("cannot open audit socket: %v", err)
77+
}
78+
return &AuditWriter{fd: fd, opened: true}, nil
79+
}
80+
81+
// Write sends payload as an AUDIT_TRUSTED_APP netlink message.
82+
// The returned byte count reflects only the original payload length.
83+
// Concurrent use requires external synchronization.
84+
func (aw *AuditWriter) Write(payload []byte) (int, error) {
85+
if !aw.opened {
86+
return 0, fmt.Errorf("cannot send audit message: not open")
87+
}
88+
msg := aw.buildMessage(payload)
89+
addr := &syscall.SockaddrNetlink{
90+
Family: syscall.AF_NETLINK,
91+
Pid: 0, // kernel
92+
}
93+
// TODO: request and handle ACK from kernel audit subsystem
94+
if err := sys.Sendto(aw.fd, msg, 0, addr); err != nil {
95+
return 0, fmt.Errorf("cannot send audit message: %v", err)
96+
}
97+
return len(payload), nil
98+
}
99+
100+
// Close closes the underlying netlink socket.
101+
func (aw *AuditWriter) Close() error {
102+
if !aw.opened {
103+
return fmt.Errorf("cannot close audit writer: not open")
104+
}
105+
aw.opened = false
106+
return sys.Close(aw.fd)
107+
}
108+
109+
// buildMessage constructs a raw netlink message containing the given payload.
110+
// The header layout follows struct nlmsghdr from
111+
// https://github.com/torvalds/linux/blob/254f49634ee16a731174d2ae34bc50bd5f45e731/include/uapi/linux/netlink.h#L45
112+
func (aw *AuditWriter) buildMessage(payload []byte) []byte {
113+
// The kernel forcibly null-terminates the payload data. We include an extra
114+
// byte to avoid overwriting message data.
115+
totalLen := nlmsgAlign(syscall.SizeofNlMsghdr + uint32(len(payload)) + 1)
116+
buf := make([]byte, totalLen)
117+
118+
// Write header in native byte order (netlink uses host endianness).
119+
// [0:4] uint32 Length of message including header
120+
// [4:6] uint16 Message content type
121+
// [6:8] uint16 Flags
122+
// [8:12] uint32 Sequence number
123+
// [12:16] uint32 Sending process port ID
124+
// TODO: Upgrade from fire-and-forget to use NLM_F_ACK and handle
125+
// acknowledgments.
126+
arch.Endian().PutUint32(buf[0:4], totalLen)
127+
arch.Endian().PutUint16(buf[4:6], auditTrustedApp)
128+
arch.Endian().PutUint16(buf[6:8], syscall.NLM_F_REQUEST)
129+
arch.Endian().PutUint32(buf[8:12], aw.nextSeq())
130+
arch.Endian().PutUint32(buf[12:16], 0)
131+
132+
// Write payload.
133+
copy(buf[syscall.SizeofNlMsghdr:], payload)
134+
return buf
135+
}
136+
137+
// nlmsgAlign rounds up to the nearest 4-byte boundary per NLMSG_ALIGN.
138+
func nlmsgAlign(size uint32) uint32 {
139+
return (size + 3) &^ 3
140+
}
141+
142+
// nextSeq returns the next non-zero sequence number, skipping zero on
143+
// wrap to allow unambiguous ACK matching (mirrors audit-userspace).
144+
func (aw *AuditWriter) nextSeq() uint32 {
145+
s := atomic.AddUint32(&aw.seq, 1)
146+
if s == 0 {
147+
s = atomic.AddUint32(&aw.seq, 1)
148+
}
149+
return s
150+
}

0 commit comments

Comments
 (0)