Skip to content

Commit

Permalink
feat: API versioning for user/kernel boundary
Browse files Browse the repository at this point in the history
This allows reusing eBPF probes across different consumers
(and consumer versions) as long as the API is compatible.

It also adds validation of API versions in the non-eBPF
kernel probes, which was a long-standing omission. Since
the actual interface evolved slowly, it generally worked fine,
but in some rare cases it could easily end up with a kernel
panic.

Signed-off-by: Grzegorz Nosek <grzegorz.nosek@sysdig.com>
  • Loading branch information
gnosek committed Feb 2, 2022
1 parent 2750b88 commit d884600
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 9 deletions.
1 change: 1 addition & 0 deletions driver/API_VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.0
18 changes: 18 additions & 0 deletions driver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ endif()
# after the build we copy the compiled module one directory up,
# to ${CMAKE_CURRENT_BINARY_DIR}.

file(STRINGS API_VERSION PROBE_API_VERSION LIMIT_COUNT 1)
string(REGEX MATCHALL "[0-9]+" PROBE_API_COMPONENTS "${PROBE_API_VERSION}")
list(GET PROBE_API_COMPONENTS 0 PPM_API_CURRENT_VERSION_MAJOR)
list(GET PROBE_API_COMPONENTS 1 PPM_API_CURRENT_VERSION_MINOR)
list(GET PROBE_API_COMPONENTS 2 PPM_API_CURRENT_VERSION_PATCH)
message(STATUS "Probe API version ${PPM_API_CURRENT_VERSION_MAJOR}.${PPM_API_CURRENT_VERSION_MINOR}.${PPM_API_CURRENT_VERSION_PATCH}")

file(STRINGS SCHEMA_VERSION PROBE_SCHEMA_VERSION LIMIT_COUNT 1)
string(REGEX MATCHALL "[0-9]+" PROBE_SCHEMA_COMPONENTS "${PROBE_SCHEMA_VERSION}")
list(GET PROBE_SCHEMA_COMPONENTS 0 PPM_SCHEMA_CURRENT_VERSION_MAJOR)
list(GET PROBE_SCHEMA_COMPONENTS 1 PPM_SCHEMA_CURRENT_VERSION_MINOR)
list(GET PROBE_SCHEMA_COMPONENTS 2 PPM_SCHEMA_CURRENT_VERSION_PATCH)
message(STATUS "Probe schema version ${PPM_SCHEMA_CURRENT_VERSION_MAJOR}.${PPM_SCHEMA_CURRENT_VERSION_MINOR}.${PPM_SCHEMA_CURRENT_VERSION_PATCH}")

execute_process(COMMAND git rev-parse HEAD OUTPUT_VARIABLE GIT_COMMIT ERROR_QUIET WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
string(STRIP "${GIT_COMMIT}" GIT_COMMIT)

configure_file(dkms.conf.in src/dkms.conf)
configure_file(Makefile.in src/Makefile)
configure_file(driver_config.h.in src/driver_config.h)
Expand All @@ -51,6 +68,7 @@ set(DRIVER_SOURCES
kernel_hacks.h
main.c
ppm.h
ppm_api_version.h
ppm_events.c
ppm_events.h
ppm_events_public.h
Expand Down
25 changes: 25 additions & 0 deletions driver/README.API_VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# API version number

The file API_VERSION must contain a semver-like version number of the userspace<->kernel API. All other lines are ignored.

## When to increment

**major version**: increment when the probe API becomes incompatible with previous userspace versions

**minor version**: increment when new features are added but existing features remain compatible

**patch version**: increment when code changes don't break compatibility (e.g. bug fixes)

Do *not* increment for patches that only add support for new kernels, without affecting already supported ones.

# Schema version number

The file SCHEMA_VERSION must contain a semver-like version number of the event schema. All other lines are ignored.

## When to increment

**major version**: increment when the schema becomes incompatible with previous userspace versions

**minor version**: increment when new features are added but existing features remain compatible (e.g. new event fields or new events)

**patch version**: increment when code changes don't break compatibility (e.g. bug fixes in filler code)
1 change: 1 addition & 0 deletions driver/SCHEMA_VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.0
6 changes: 6 additions & 0 deletions driver/bpf/probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,9 @@ char kernel_ver[] __bpf_section("kernel_version") = UTS_RELEASE;
char __license[] __bpf_section("license") = "GPL";

char probe_ver[] __bpf_section("probe_version") = PROBE_VERSION;

char probe_commit[] __bpf_section("build_commit") = PROBE_COMMIT;

uint64_t probe_api_ver __bpf_section("api_version") = PPM_API_CURRENT_VERSION;

uint64_t probe_schema_ver __bpf_section("schema_version") = PPM_SCHEMA_CURRENT_VERSION;
14 changes: 14 additions & 0 deletions driver/driver_config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@ or GPL2.txt for full copies of the license.
*/
#pragma once

/* taken from driver/API_VERSION */
#define PPM_API_CURRENT_VERSION_MAJOR ${PPM_API_CURRENT_VERSION_MAJOR}
#define PPM_API_CURRENT_VERSION_MINOR ${PPM_API_CURRENT_VERSION_MINOR}
#define PPM_API_CURRENT_VERSION_PATCH ${PPM_API_CURRENT_VERSION_PATCH}

/* taken from driver/SCHEMA_VERSION */
#define PPM_SCHEMA_CURRENT_VERSION_MAJOR ${PPM_SCHEMA_CURRENT_VERSION_MAJOR}
#define PPM_SCHEMA_CURRENT_VERSION_MINOR ${PPM_SCHEMA_CURRENT_VERSION_MINOR}
#define PPM_SCHEMA_CURRENT_VERSION_PATCH ${PPM_SCHEMA_CURRENT_VERSION_PATCH}

#include "ppm_api_version.h"

#define PROBE_VERSION "${PROBE_VERSION}"

#define PROBE_NAME "${PROBE_NAME}"

#define PROBE_DEVICE_NAME "${PROBE_DEVICE_NAME}"

#define PROBE_COMMIT "${GIT_COMMIT}"

#ifndef KBUILD_MODNAME
#define KBUILD_MODNAME PROBE_NAME
#endif
9 changes: 9 additions & 0 deletions driver/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,12 @@ static long ppm_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
}
ret = 0;
goto cleanup_ioctl_nolock;
} else if (cmd == PPM_IOCTL_GET_API_VERSION) {
ret = PPM_API_CURRENT_VERSION;
goto cleanup_ioctl_nolock;
} else if (cmd == PPM_IOCTL_GET_SCHEMA_VERSION) {
ret = PPM_SCHEMA_CURRENT_VERSION;
goto cleanup_ioctl_nolock;
}

mutex_lock(&g_consumer_mutex);
Expand Down Expand Up @@ -2632,6 +2638,9 @@ void sysdig_exit(void)
module_init(sysdig_init);
module_exit(sysdig_exit);
MODULE_VERSION(PROBE_VERSION);
MODULE_INFO(build_commit, PROBE_COMMIT);
MODULE_INFO(api_version, PPM_API_CURRENT_VERSION_STRING);
MODULE_INFO(schema_version, PPM_SCHEMA_CURRENT_VERSION_STRING);

module_param(max_consumers, uint, 0444);
MODULE_PARM_DESC(max_consumers, "Maximum number of consumers that can simultaneously open the devices");
Expand Down
48 changes: 48 additions & 0 deletions driver/ppm_api_version.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#ifndef PPM_API_VERSION_H
#define PPM_API_VERSION_H

/*
* API version component macros
*
* The version is a single uint64_t, structured as follows:
* bit 63: unused (so the version number is always positive)
* bits 44-62: major version
* bits 24-43: minor version
* bits 0-23: patch version
*/

/* extract components from an API version number */
#define PPM_API_VERSION_MAJOR(ver) ((((ver) >> 44)) & (((1 << 19) - 1)))
#define PPM_API_VERSION_MINOR(ver) (((ver) >> 24) & (((1 << 20) - 1)))
#define PPM_API_VERSION_PATCH(ver) (((ver) & ((1 << 24) - 1)))

/* build an API version number from components */
#define PPM_API_VERSION(major, minor, patch) \
(((major) & (((1ULL << 19) - 1) << 44)) | \
((minor) & (((1ULL << 20) - 1) << 24)) | \
((major) & (((1ULL << 24) - 1))))

#define PPM_API_CURRENT_VERSION PPM_API_VERSION( \
PPM_API_CURRENT_VERSION_MAJOR, \
PPM_API_CURRENT_VERSION_MINOR, \
PPM_API_CURRENT_VERSION_PATCH)

#define PPM_SCHEMA_CURRENT_VERSION PPM_API_VERSION( \
PPM_SCHEMA_CURRENT_VERSION_MAJOR, \
PPM_SCHEMA_CURRENT_VERSION_MINOR, \
PPM_SCHEMA_CURRENT_VERSION_PATCH)

#define __PPM_STRINGIFY1(x) #x
#define __PPM_STRINGIFY(x) __PPM_STRINGIFY1(x)

#define PPM_API_CURRENT_VERSION_STRING \
__PPM_STRINGIFY(PPM_API_CURRENT_VERSION_MAJOR) "." \
__PPM_STRINGIFY(PPM_API_CURRENT_VERSION_MINOR) "." \
__PPM_STRINGIFY(PPM_API_CURRENT_VERSION_PATCH)

#define PPM_SCHEMA_CURRENT_VERSION_STRING \
__PPM_STRINGIFY(PPM_SCHEMA_CURRENT_VERSION_MAJOR) "." \
__PPM_STRINGIFY(PPM_SCHEMA_CURRENT_VERSION_MINOR) "." \
__PPM_STRINGIFY(PPM_SCHEMA_CURRENT_VERSION_PATCH)

#endif
2 changes: 2 additions & 0 deletions driver/ppm_events_public.h
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,8 @@ struct ppm_evt_hdr {
#define PPM_IOCTL_GET_PROBE_VERSION _IO(PPM_IOCTL_MAGIC, 21)
#define PPM_IOCTL_SET_FULLCAPTURE_PORT_RANGE _IO(PPM_IOCTL_MAGIC, 22)
#define PPM_IOCTL_SET_STATSD_PORT _IO(PPM_IOCTL_MAGIC, 23)
#define PPM_IOCTL_GET_API_VERSION _IO(PPM_IOCTL_MAGIC, 24)
#define PPM_IOCTL_GET_SCHEMA_VERSION _IO(PPM_IOCTL_MAGIC, 25)
#endif // CYGWING_AGENT

extern const struct ppm_name_value socket_families[];
Expand Down
10 changes: 10 additions & 0 deletions userspace/libscap/scap-int.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ struct scap

// The return value from the last call to next_batch().
ss_plugin_rc m_input_plugin_last_batch_res;

// API version supported by the probe
// If the API version is unavailable for whatever reason,
// it's equivalent to version 0.0.0
uint64_t m_api_version;

// schema version supported by the probe
// If the schema version is unavailable for whatever reason,
// it's equivalent to version 0.0.0
uint64_t m_schema_version;
};

typedef enum ppm_dumper_type
Expand Down
133 changes: 132 additions & 1 deletion userspace/libscap/scap.c
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ scap_t* scap_open_live_int(char *error, int32_t *rc,
{
int len;
uint32_t all_scanned_devs;
uint64_t api_version;
uint64_t schema_version;

//
// Allocate the device descriptors.
Expand Down Expand Up @@ -427,10 +429,72 @@ scap_t* scap_open_live_int(char *error, int32_t *rc,
// Set close-on-exec for the fd
if (fcntl(handle->m_devs[j].m_fd, F_SETFD, FD_CLOEXEC) == -1) {
snprintf(error, SCAP_LASTERR_SIZE, "Can not set close-on-exec flag for fd for device %s (%s)", filename, scap_strerror(handle, errno));
close(handle->m_devs[j].m_fd);
scap_close(handle);
*rc = SCAP_FAILURE;
return NULL;
}

// Check the API version reported
api_version = ioctl(handle->m_devs[j].m_fd, PPM_IOCTL_GET_API_VERSION, 0);
if ((int64_t)api_version < 0)
{
snprintf(error, SCAP_LASTERR_SIZE, "Kernel module does not support PPM_IOCTL_GET_API_VERSION");
close(handle->m_devs[j].m_fd);
scap_close(handle);
*rc = SCAP_FAILURE;
return NULL;
}
// Make sure all devices report the same API version
if (handle->m_api_version != 0 && handle->m_api_version != api_version)
{
snprintf(error, SCAP_LASTERR_SIZE, "API version mismatch: device %s reports API version %lu.%lu.%lu, expected %lu.%lu.%lu",
filename,
PPM_API_VERSION_MAJOR(api_version),
PPM_API_VERSION_MINOR(api_version),
PPM_API_VERSION_PATCH(api_version),
PPM_API_VERSION_MAJOR(handle->m_api_version),
PPM_API_VERSION_MINOR(handle->m_api_version),
PPM_API_VERSION_PATCH(handle->m_api_version)
);
close(handle->m_devs[j].m_fd);
scap_close(handle);
*rc = SCAP_FAILURE;
return NULL;
}
// Set the API version from the first device
// (for subsequent devices it's a no-op thanks to the check above)
handle->m_api_version = api_version;

// Check the schema version reported
schema_version = ioctl(handle->m_devs[j].m_fd, PPM_IOCTL_GET_SCHEMA_VERSION, 0);
if ((int64_t)schema_version < 0)
{
snprintf(error, SCAP_LASTERR_SIZE, "Kernel module does not support PPM_IOCTL_GET_SCHEMA_VERSION");
close(handle->m_devs[j].m_fd);
scap_close(handle);
*rc = SCAP_FAILURE;
return NULL;
}
// Make sure all devices report the same schema version
if (handle->m_schema_version != 0 && handle->m_schema_version != schema_version)
{
snprintf(error, SCAP_LASTERR_SIZE, "Schema version mismatch: device %s reports schema version %lu.%lu.%lu, expected %lu.%lu.%lu",
filename,
PPM_API_VERSION_MAJOR(schema_version),
PPM_API_VERSION_MINOR(schema_version),
PPM_API_VERSION_PATCH(schema_version),
PPM_API_VERSION_MAJOR(handle->m_schema_version),
PPM_API_VERSION_MINOR(handle->m_schema_version),
PPM_API_VERSION_PATCH(handle->m_schema_version)
);
scap_close(handle);
*rc = SCAP_FAILURE;
return NULL;
}
// Set the schema version from the first device
// (for subsequent devices it's a no-op thanks to the check above)
handle->m_schema_version = schema_version;

//
// Map the ring buffer
Expand Down Expand Up @@ -496,6 +560,34 @@ scap_t* scap_open_live_int(char *error, int32_t *rc,
}
}

if(!scap_is_api_compatible(handle->m_api_version, SCAP_MINIMUM_PROBE_API_VERSION))
{
snprintf(error, SCAP_LASTERR_SIZE, "Probe supports API version %lu.%lu.%lu, but running version needs %d.%d.%d",
PPM_API_VERSION_MAJOR(handle->m_api_version),
PPM_API_VERSION_MINOR(handle->m_api_version),
PPM_API_VERSION_PATCH(handle->m_api_version),
PPM_API_CURRENT_VERSION_MAJOR,
PPM_API_CURRENT_VERSION_MINOR,
PPM_API_CURRENT_VERSION_PATCH);
*rc = SCAP_FAILURE;
scap_close(handle);
return NULL;
}

if(!scap_is_api_compatible(handle->m_schema_version, SCAP_MINIMUM_PROBE_SCHEMA_VERSION))
{
snprintf(error, SCAP_LASTERR_SIZE, "Probe supports schema version %lu.%lu.%lu, but running version needs %d.%d.%d",
PPM_API_VERSION_MAJOR(handle->m_schema_version),
PPM_API_VERSION_MINOR(handle->m_schema_version),
PPM_API_VERSION_PATCH(handle->m_schema_version),
PPM_SCHEMA_CURRENT_VERSION_MAJOR,
PPM_SCHEMA_CURRENT_VERSION_MINOR,
PPM_SCHEMA_CURRENT_VERSION_PATCH);
*rc = SCAP_FAILURE;
scap_close(handle);
return NULL;
}

for(j = 0; j < handle->m_ndevs; ++j)
{
//
Expand Down Expand Up @@ -2968,4 +3060,43 @@ int32_t scap_set_statsd_port(scap_t* const handle, const uint16_t port)

return SCAP_SUCCESS;
#endif
}
}

bool scap_is_api_compatible(unsigned long probe_api_version, unsigned long required_api_version)
{
unsigned long probe_major = PPM_API_VERSION_MAJOR(probe_api_version);
unsigned long probe_minor = PPM_API_VERSION_MINOR(probe_api_version);
unsigned long probe_patch = PPM_API_VERSION_PATCH(probe_api_version);
unsigned long required_major = PPM_API_VERSION_MAJOR(required_api_version);
unsigned long required_minor = PPM_API_VERSION_MINOR(required_api_version);
unsigned long required_patch = PPM_API_VERSION_PATCH(required_api_version);

if(probe_major != required_major)
{
// major numbers disagree
return false;
}

if(probe_minor < required_minor)
{
// probe's minor version is < ours
return false;
}
if(probe_minor == required_minor && probe_patch < required_patch)
{
// probe's minor versions match and patch level is < ours
return false;
}

return true;
}

uint64_t scap_get_probe_api_version(scap_t* handle)
{
return handle->m_api_version;
}

uint64_t scap_get_probe_schema_version(scap_t* handle)
{
return handle->m_schema_version;
}
Loading

0 comments on commit d884600

Please sign in to comment.