Files
Juergen Gross 6b30eca0e3 tools/xenstored: fix live update with watches specifying depth
Due to a typo the state record for a watch specifying a depth is
written only partially. Instead of using the correct length of the
xs_state_watch_ext structure only the length of the xs_state_watch
structure was used when writing the data.

Fix that by using the correct length.

This is XSA-514 / CVE-2026-79607

Fixes: db98de120c ("tools/xenstored: add depth information to watches")
Reported-by: Andrii Sultanov <andriy.sultanov@vates.tech>
Signed-off-by: Juergen Gross <jgross@suse.com>
2026-10-13 13:47:27 +01:00

423 lines
10 KiB
C

/*
Watch code for Xen Store Daemon.
Copyright (C) 2005 Rusty Russell IBM Corporation
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <sys/types.h>
#include <stdarg.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <assert.h>
#include "talloc.h"
#include "list.h"
#include "watch.h"
#include "xenstore_lib.h"
#include "utils.h"
#include "domain.h"
#include "transaction.h"
struct watch
{
/* Watches on this connection */
struct list_head list;
/* Offset into path for skipping prefix (used for relative paths). */
unsigned int prefix_len;
int depth; /* -1: no depth limit. */
char *token;
char *node;
};
/* Is child a subnode of parent, or equal? */
static bool is_child(const char *child, const char *parent, int depth)
{
unsigned int len = strlen(parent); /* len == 1 if parent is "/". */
unsigned int sub_levels = (len == 1) ? 1 : 0;
if (strncmp(child, parent, len) != 0)
return false;
if (child[len] != '/' && child[len] != '\0' && len > 1)
return false;
if (depth < 0 || child[len] == '\0')
return true;
while (sub_levels <= depth) {
if (child[len] == '\0')
return true;
if (child[len] == '/')
sub_levels++;
len++;
}
return false;
}
static const char *get_watch_path(const struct watch *watch, const char *name)
{
return name + watch->prefix_len;
}
/*
* Check permissions of a specific watch to fire:
* Either the node itself or its parent have to be readable by the connection
* the watch has been setup for. In case a watch event is created due to
* changed permissions we need to take the old permissions into account, too.
*/
static bool watch_permitted(struct connection *conn, const void *ctx,
const char *name, const struct node *node,
struct node_perms *perms)
{
unsigned int perm;
const struct node *parent;
char *parent_name;
if (perms) {
perm = perm_for_conn(conn, perms);
if (perm & XS_PERM_READ)
return true;
}
if (!node) {
node = read_node_const(conn, ctx, name);
if (!node)
return false;
}
perm = perm_for_conn_from_node(conn, node);
if (perm & XS_PERM_READ)
return true;
parent = node->parent;
if (!parent) {
parent_name = get_parent(ctx, node->name);
if (!parent_name)
return false;
parent = read_node_const(conn, ctx, parent_name);
if (!parent)
return false;
}
perm = perm_for_conn_from_node(conn, parent);
return perm & XS_PERM_READ;
}
/*
* Check whether any watch events are to be sent.
* Temporary memory allocations are done with ctx.
* We need to take the (potential) old permissions of the node into account
* as a watcher losing permissions to access a node should receive the
* watch event, too.
*/
void fire_watches(struct connection *conn, const void *ctx, const char *name,
const struct node *node, enum watch_match match,
struct node_perms *perms)
{
struct connection *i;
struct buffered_data *req;
struct watch *watch;
/* During transactions, don't fire watches, but queue them. */
if (conn && conn->transaction) {
queue_watches(conn, name, match);
return;
}
req = domain_is_unprivileged(conn) ? conn->in : NULL;
/* Create an event for each watch. */
list_for_each_entry(i, &connections, list) {
if (!watch_permitted(i, ctx, name, node, perms))
continue;
list_for_each_entry(watch, &i->watches, list) {
bool send = false;
switch (match) {
case MATCH_EXACT:
send = streq(name, watch->node);
break;
case MATCH_SUBTREE:
send = is_child(name, watch->node,
watch->depth);
break;
case MATCH_DEPTH:
send = streq(name, watch->node) ||
(watch->depth > 0 &&
is_child(name, watch->node,
watch->depth));
break;
case MATCH_NODEPTH:
send = streq(name, watch->node) &&
watch->depth < 0;
break;
}
if (send)
send_event(req, i, get_watch_path(watch, name),
watch->token);
}
}
}
static int destroy_watch(void *_watch)
{
trace_destroy(_watch, "watch");
return 0;
}
static int check_watch_path(struct connection *conn, const void *ctx,
const char **path, bool *relative)
{
*relative = !strstarts(*path, "/") && !strstarts(*path, "@");
*path = canonicalize(conn, ctx, *path, true);
return *path ? 0 : errno;
}
static struct watch *add_watch(struct connection *conn, const char *path,
const char *token, int depth, bool relative,
bool no_quota_check)
{
struct watch *watch;
watch = talloc(conn, struct watch);
if (!watch)
goto nomem;
watch->depth = depth;
watch->node = talloc_strdup(watch, path);
watch->token = talloc_strdup(watch, token);
if (!watch->node || !watch->token)
goto nomem;
if (domain_memory_add(conn, conn->id, strlen(path) + strlen(token),
no_quota_check))
goto nomem;
watch->prefix_len = relative ? strlen(get_implicit_path(conn)) + 1 : 0;
domain_watch_inc(conn);
list_add_tail(&watch->list, &conn->watches);
talloc_set_destructor(watch, destroy_watch);
return watch;
nomem:
talloc_free(watch);
errno = ENOMEM;
return NULL;
}
int do_watch(const void *ctx, struct connection *conn, struct buffered_data *in)
{
struct watch *watch;
const char *vec[3];
unsigned int n_pars;
int depth = -1;
bool relative;
n_pars = get_strings(in, vec, ARRAY_SIZE(vec));
if (n_pars == 3 &&
feature_available(conn, XENSTORE_SERVER_FEATURE_WATCHDEPTH)) {
depth = atoi(vec[2]);
if (depth < 0)
return EINVAL;
} else if (n_pars != 2)
return EINVAL;
errno = check_watch_path(conn, ctx, &(vec[0]), &relative);
if (errno)
return errno;
/* Check for duplicates. */
list_for_each_entry(watch, &conn->watches, list) {
if (streq(watch->node, vec[0]) &&
streq(watch->token, vec[1]))
return EEXIST;
}
if (domain_quota_add_exceeds(conn->domain, ACC_WATCH, 1))
return ENOSPC;
watch = add_watch(conn, vec[0], vec[1], depth, relative, false);
if (!watch)
return errno;
trace_create(watch, "watch");
send_ack(conn, XS_WATCH);
/*
* We fire once up front: simplifies clients and restart.
* This event will not be linked to the XS_WATCH request.
*/
send_event(NULL, conn, get_watch_path(watch, watch->node),
watch->token);
return 0;
}
int do_unwatch(const void *ctx, struct connection *conn,
struct buffered_data *in)
{
struct watch *watch;
const char *node;
const char *vec[2];
if (get_strings(in, vec, ARRAY_SIZE(vec)) != ARRAY_SIZE(vec))
return EINVAL;
node = canonicalize(conn, ctx, vec[0], true);
if (!node)
return errno;
list_for_each_entry(watch, &conn->watches, list) {
if (streq(watch->node, node) && streq(watch->token, vec[1])) {
list_del(&watch->list);
domain_memory_add_nochk(conn, conn->id,
-strlen(watch->node) -
strlen(watch->token));
talloc_free(watch);
domain_watch_dec(conn);
send_ack(conn, XS_UNWATCH);
return 0;
}
}
return ENOENT;
}
void conn_delete_all_watches(struct connection *conn)
{
struct watch *watch;
while ((watch = list_top(&conn->watches, struct watch, list))) {
list_del(&watch->list);
domain_memory_add_nochk(conn, conn->id, -strlen(watch->node) -
strlen(watch->token));
talloc_free(watch);
domain_watch_dec(conn);
}
}
const char *dump_state_watches(FILE *fp, struct connection *conn,
unsigned int conn_id)
{
const char *ret = NULL;
struct watch *watch;
struct xs_state_watch sw;
struct xs_state_watch_ext swe;
struct xs_state_record_header head;
const char *path;
size_t path_len, token_len;
list_for_each_entry(watch, &conn->watches, list) {
path = get_watch_path(watch, watch->node);
path_len = strlen(path) + 1;
token_len = strlen(watch->token) + 1;
if (watch->depth >= 0) {
head.type = XS_STATE_TYPE_WATCH_EXT;
head.length = sizeof(swe);
swe.conn_id = conn_id;
swe.path_length = path_len;
swe.token_length = token_len;
swe.depth = watch->depth;
} else {
head.type = XS_STATE_TYPE_WATCH;
head.length = sizeof(sw);
sw.conn_id = conn_id;
sw.path_length = path_len;
sw.token_length = token_len;
}
head.length += path_len + token_len;
head.length = ROUNDUP(head.length, 8);
if (fwrite(&head, sizeof(head), 1, fp) != 1)
return "Dump watch state error";
if (watch->depth >= 0) {
if (fwrite(&swe, sizeof(swe), 1, fp) != 1)
return "Dump watch state ext error";
} else {
if (fwrite(&sw, sizeof(sw), 1, fp) != 1)
return "Dump watch state error";
}
if (fwrite(path, path_len, 1, fp) != 1)
return "Dump watch path error";
if (fwrite(watch->token, token_len, 1, fp) != 1)
return "Dump watch token error";
ret = dump_state_align(fp);
if (ret)
return ret;
}
return ret;
}
static void process_state_watch(const void *ctx, unsigned int conn_id,
const char *path, const char *token,
int depth)
{
struct connection *conn;
bool relative;
conn = get_connection_by_id(conn_id);
if (!conn)
barf("connection not found for read watch");
/* Don't check success, we want the relative information only. */
check_watch_path(conn, ctx, &path, &relative);
if (!path)
barf("allocation error for read watch");
if (!add_watch(conn, path, token, depth, relative, true))
barf("error adding watch");
}
void read_state_watch(const void *ctx, const void *state)
{
const struct xs_state_watch *sw = state;
process_state_watch(ctx, sw->conn_id, (char *)sw->data,
(char *)sw->data + sw->path_length, -1);
}
void read_state_watch_ext(const void *ctx, const void *state)
{
const struct xs_state_watch_ext *swe = state;
process_state_watch(ctx, swe->conn_id, (char *)swe->data,
(char *)swe->data + swe->path_length, swe->depth);
}
/*
* Local variables:
* mode: C
* c-file-style: "linux"
* indent-tabs-mode: t
* c-basic-offset: 8
* tab-width: 8
* End:
*/