[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [PATCH v2 10/12] tools/libxl: add support for xenstore quota in domain_config





On Fri, Mar 20, 2026 at 3:02 PM Juergen Gross <jgross@xxxxxxxx> wrote:
Add support for xenstore quota in the struct domain_config. Initially
it will be used only for migration of a domain.

Signed-off-by: Juergen Gross <jgross@xxxxxxxx>
Acked-by: Nick Rosbrook <enr0n@xxxxxxxxxx> # golang stuff
---
V2:
- use LOGED() for error logging (Anthony Perard)
- mention additional struct member xenstore_quota in libxl.h
  (Anthony Perard)
---
 tools/golang/xenlight/helpers.gen.go |  6 ++++++
 tools/golang/xenlight/types.gen.go   |  1 +
 tools/include/libxl.h                |  1 +
 tools/libs/light/libxl_dom.c         |  8 ++++++++
 tools/libs/light/libxl_domain.c      | 11 +++++++++++
 tools/libs/light/libxl_types.idl     |  1 +
 6 files changed, 28 insertions(+)

diff --git a/tools/golang/xenlight/helpers.gen.go b/tools/golang/xenlight/helpers.gen.go
index 767b9e45f5..b0c09da910 100644
--- a/tools/golang/xenlight/helpers.gen.go
+++ b/tools/golang/xenlight/helpers.gen.go
@@ -1286,6 +1286,9 @@ if err := x.TrapUnmappedAccesses.fromC(&xc.trap_unmapped_accesses);err != nil {
 return fmt.Errorf("converting field TrapUnmappedAccesses: %v", err)
 }
 x.XenstoreFeatureMask = uint32(xc.xenstore_feature_mask)
+if err := x.XenstoreQuota.fromC(&xc.xenstore_quota);err != nil {
+return fmt.Errorf("converting field XenstoreQuota: %v", err)
+}

  return nil}

@@ -1825,6 +1828,9 @@ if err := x.TrapUnmappedAccesses.toC(&xc.trap_unmapped_accesses); err != nil {
 return fmt.Errorf("converting field TrapUnmappedAccesses: %v", err)
 }
 xc.xenstore_feature_mask = C.uint32_t(x.XenstoreFeatureMask)
+if err := x.XenstoreQuota.toC(&xc.xenstore_quota); err != nil {
+return fmt.Errorf("converting field XenstoreQuota: %v", err)
+}

  return nil
  }
diff --git a/tools/golang/xenlight/types.gen.go b/tools/golang/xenlight/types.gen.go
index 8dd610919d..e0fd78ec03 100644
--- a/tools/golang/xenlight/types.gen.go
+++ b/tools/golang/xenlight/types.gen.go
@@ -629,6 +629,7 @@ VmtraceBufKb int
 Vpmu Defbool
 TrapUnmappedAccesses Defbool
 XenstoreFeatureMask uint32
+XenstoreQuota XsQuotaList
 }

 type DomainBuildInfoTypeUnion interface {
diff --git a/tools/include/libxl.h b/tools/include/libxl.h
index 6d2910df34..80e3ec8de9 100644
--- a/tools/include/libxl.h
+++ b/tools/include/libxl.h
@@ -1545,6 +1545,7 @@ void libxl_mac_copy(libxl_ctx *ctx, libxl_mac *dst, const libxl_mac *src);
  * libxl_xs_quota_global_set()
  * libxl_xs_quota_domain_get()
  * libxl_xs_quota_domain_set()
+ * and the xenstore_quota member of struct domain_build_info
  * are available.
  */
 #define LIBXL_HAVE_XENSTORE_QUOTA
diff --git a/tools/libs/light/libxl_dom.c b/tools/libs/light/libxl_dom.c
index 05ebc69534..4ff5f65f6f 100644
--- a/tools/libs/light/libxl_dom.c
+++ b/tools/libs/light/libxl_dom.c
@@ -509,6 +509,14 @@ retry_transaction:

     xs_introduce_domain(ctx->xsh, domid, state->store_mfn, state->store_port);

+    if (info->xenstore_quota.num_quota) {
+        rc = libxl_xs_quota_domain_set(ctx, domid, &info->xenstore_quota);
+        if (rc) {
+            LOGED(ERROR, domid, "Failed to set Xenstore quota");
+            goto out;
+        }
+    }
+
  out:
     free(vm_path);
     return rc;
diff --git a/tools/libs/light/libxl_domain.c b/tools/libs/light/libxl_domain.c
index 5be47f687f..37fcd92871 100644
--- a/tools/libs/light/libxl_domain.c
+++ b/tools/libs/light/libxl_domain.c
@@ -2533,6 +2533,17 @@ static void retrieve_domain_configuration_end(libxl__egc *egc,
         }
     }

+    /* Xenstore quota */
+    {
+        libxl_xs_quota_list_dispose(&d_config->b_info.xenstore_quota);
+        rc = libxl_xs_quota_domain_get(CTX, domid,
+                                       &d_config->b_info.xenstore_quota);
+        if (rc) {
+            LOGED(ERROR, domid, "Fail to get xenstore quota");
+            goto out;
+        }

This change seems to cause `xl list` to hang on oxenstored systems.

I'm not an expert in ocaml; Claude seems to think that the problem is as follows:

1. oxenstored doesn't implement XS_GET_QUOTA, and
2. when it receives an unknown message it returns an error, but
3. the error payload is length zero, and
4. oxenstored has a bug such that it will never actually send messages with a zero-length payload.

Below is a more complete report from Claude; as I said, I don't know enough ocaml to evaluate it. Plan `xl list` only triggers it on staging, but apparently `xl list -x` will trigger it in Xen 4.21.

Assuming the analysis is correct, it's concerning that an issue with the default xenstored instance wasn't caught earlier.

 -George

8<---

Subject: oxenstored hangs on any xenstore request with a zero-length payload

  Affects, on oxenstored systems:
    - `xl info -x` and `xenstore watch -d N`  -- RELEASE-4.21.0, RELEASE-4.21.1
    - `xl list -l`, `xl save`, `xl migrate`   -- 4.22.0-rc1..rc3, unstable
  The C xenstored and xenstore-stubdom are unaffected.


Symptom
-------

On an idle dom0 with no guests, running oxenstored, `xl list -l` never
returns and produces no output. The same holds for `xl save`, `xl migrate`,
`xl info -x`, and `xenstore watch -d N`. Meanwhile `xl list`, `xl list -v`,
`xl info -n`, `xenstore-read` and `xenstore-ls` all work normally.

The client is blocked in read(fd, buf, 16) awaiting an xsd_sockmsg header
that never arrives. Both socket queues are empty. oxenstored itself remains
healthy and continues to serve every other connection.


Root cause
----------

oxenstored's socket backend never delivers a request whose payload length
is zero. This is independent of the opcode: XS_READ and XS_DIRECTORY with
len == 0 hang exactly as XS_GET_QUOTA does.

In tools/ocaml/libs/xb/xb.ml, Xb.input() consumes the 16-byte header and
sets partial_in <- HaveHdr pkt, then returns None unconditionally:

    | NoHdr (i, buf)      ->
        (* we complete the partial header *)
        if sz > 0 then
          Bytes.blit b 0 buf (Partial.header_size () - i) sz;
        con.partial_in <- if sz = i then
            HaveHdr (Partial.of_string (Bytes.to_string buf)) else NoHdr (i - sz, buf);
        None

The packet would be produced by the *next* call to input() -- the HaveHdr
branch handles Partial.to_complete = 0 without reading anything further.
But has_more_input is hardcoded false for socket backends:

    let has_more_input con =
      match con.backend with
      | Fd _            -> false
      | Xenmmap backend -> backend.work_again

so xenstored.ml re-enters do_input only when poll() reports the fd readable.

  - For len > 0, the payload is still buffered in the socket, so poll()
    fires again and the packet completes. This is why every other request
    works.

  - For len == 0, nothing remains to be read. poll() never fires, and a
    fully-received request sits parked in partial_in indefinitely.

It is finally processed only when further input arrives on that connection
-- including POLLHUP at client teardown -- at which point the reply is
written to a dead socket.

oxenstored's dispatch layer is correct: XS_GET_QUOTA with a non-empty
payload returns XS_ERROR "ENOSYS" in under a millisecond. Only the framing
layer is broken.

Guest ring (Xenmmap) connections appear unaffected, since there
has_more_input follows work_again. NOTE: this is a source-level inference;
I did not exercise a ring connection with a zero-length request.


Evidence
--------

strace of oxenstored. \31 = 25 = XS_GET_QUOTA, \2 = XS_READ:

  # len=0, unknown opcode 25 -- nothing written, silence, then client EOF
  14:54:01.801066 read(23, "\31\0\0\0\336\300\0\0\0\0\0\0\0\0\0\0", 16) = 16
  14:54:06.806325 write(18, "[...]  A81 invalid", 50)   <- only at POLLHUP
  14:54:06.806811 read(23, "", 16)        = 0

  # len=0, WELL-KNOWN opcode 2 -- identical hang
  14:54:10.840293 read(23, "\2\0\0\0\336\300\0\0\0\0\0\0\0\0\0\0", 16) = 16
  14:54:15.845644 read(23, "", 16)        = 0

  # same opcode 25, len=2 -- answered instantly
  14:54:19.879492 read(23, "\31\0\0\0\336\300\0\0\0\0\0\0\2\0\0\0", 16) = 16
  14:54:19.879698 read(23, "0\0", 2)      = 2
  14:54:19.879810 write(18, "[...] A83 invalid / error ENOSYS", ...)

The poll() following the parked read requests POLLIN only -- no POLLOUT --
i.e. oxenstored has nothing queued to send. xenstored.log contains neither
"process packet:" nor "got a bad client": process_packet is never entered.

Decisive: send the len == 0 header, wait three seconds (nothing), then send
one unrelated byte. The ENOSYS reply *for the original request id* is
emitted within 1 ms. The request was never dropped -- it was parked,
waiting for a poll() that had nothing left to deliver.


Trigger
-------

libxenstore has exactly two zero-length senders, both recent. Before them,
no xenstore client had ever sent a zero-length request over the socket, so
the defect lay dormant.

  xs_get_features_supported()  XS_GET_FEATURE  tools/libs/store/xs.c:1486
      added by 5234b61eab
      in: RELEASE-4.21.0, RELEASE-4.21.1, 4.22.0-rc*, unstable
      reached by: xl info -x, xenstore watch -d N

  xs_get_quota_names()         XS_GET_QUOTA    tools/libs/store/xs.c:1537
      added by ba90589447
      in: 4.22.0-rc1..rc3, unstable
      reached by: xl list -l, xl save, xl migrate -- since a6f159f92c made
      libxl_retrieve_domain_configuration() query xenstore quota

Both are the only `struct iovec iov[1]` call sites in xs.c.


libxl is not at fault
---------------------

tools/libs/light/libxl_xsquota.c explicitly treats quota as optional:

    names = xs_get_quota_names(ctx->xsh, &num);
    if (!names) {
        /* Xenstore quota support is optional! */
        if (errno != ENOSYS) { ... rc = ERROR_FAIL; }
        else                 { rc = 0; }

Confirmed empirically: with an LD_PRELOAD shim forcing xs_get_quota_names()
to fail with ENOSYS, against the same hanging oxenstored, `xl list -l`
returns rc=0 with full JSON and `xl save` writes a 2.1 GB savefile. A
xenstored that lacks opcode 25 but *answers* ENOSYS works fine. The C
xenstored answers ENOSYS to unknown opcodes at any length.


Age
---

The defect is present in oxenstored's original commit:

    c3afd398ba7f349fdf59d79aaed4da256176806a
    Keir Fraser <keir.fraser@xxxxxxxxxx>, 2010-05-06
    "ocaml: Add XS bindings."

`git log -S has_more_input -- tools/ocaml/libs/xb/xb.ml` returns that commit
and no other; has_more_input has never been modified. The NoHdr branch is
unchanged in substance since RELEASE-4.2.0, and
`git diff RELEASE-4.21.0 HEAD -- tools/ocaml/libs/xb/{xb,partial,op}.ml`
is empty -- the files are byte-identical.


Reproducer
----------

Attached: oxs-zerolen.c (50 lines, no dependencies).

  gcc -o oxs-zerolen oxs-zerolen.c && sudo ./oxs-zerolen

Against oxenstored:

  XS_READ(2)      len=21  (sanity)           -> reply type=2 len=8
  XS_GET_QUOTA(25) len=2  (unknown op)       -> reply type=16 len=7   [ENOSYS]
  XS_READ(2)      len=0   (known op!)        -> NO REPLY (5s timeout)
  XS_DIRECTORY(1) len=0   (known op!)        -> NO REPLY (5s timeout)
  XS_GET_FEATURE(23) len=0  (xl info -x)     -> NO REPLY (5s timeout)
  XS_GET_QUOTA(25) len=0  (xl list -l)       -> NO REPLY (5s timeout)

Against the C xenstored: all six reply in 0.000 s.


Fix
---

In Xb.input, return the packet as soon as a complete header with len == 0
has been read, rather than returning None and waiting for input that will
never arrive:

    | NoHdr (i, buf) ->
      if sz > 0 then Bytes.blit b 0 buf (Partial.header_size () - i) sz;
      if sz = i then begin
        let partial_pkt = Partial.of_string (Bytes.to_string buf) in
        if Partial.to_complete partial_pkt = 0 then begin
          con.partial_in <- init_partial_in ();
          Some (Packet.of_partialpkt partial_pkt)      (* was: None *)
        end else (con.partial_in <- HaveHdr partial_pkt; None)
      end else (con.partial_in <- NoHdr (i - sz, buf); None)

Alternatively, fix has_more_input, which is where the invariant is actually
violated -- "a complete packet is buffered" ought to imply more work is
pending. This duplicates no packet construction, since the HaveHdr branch
already handles to_complete = 0 with no read:

    let has_more_input con =
      match con.backend with
      | Fd _ -> (match con.partial_in with
                 | HaveHdr p -> Partial.to_complete p = 0
                 | _ -> false)
      | Xenmmap backend -> backend.work_again

The Fd arm being a hardcoded `false` while Xenmmap consults work_again is
the asymmetry that hid this.

Tested on
---------

Xen 4.23-unstable, git e58a8e0889, debug=n, bare metal (Debian 13, x86_64).
oxenstored built from the same tree; running binary sha256-verified against
tools/ocaml/xenstored/oxenstored.

Not tested: 4.21.0 / 4.22-rc binaries were not built and run. The claim that
they are affected rests on (a) the xb library being byte-identical to HEAD
and (b) 5234b61eab / ba90589447 being present in those tags. Both were
checked with git; neither was exercised.


---oxs-zerolen.c
/*
 * oxs-zerolen.c - oxenstored never answers a request with len == 0.
 *
 *   gcc -o oxs-zerolen oxs-zerolen.c && sudo ./oxs-zerolen
 *
 * Against oxenstored: every len==0 request times out, whatever the opcode.
 * Against C xenstored: every case replies immediately.
 */
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>

struct xsd_sockmsg { uint32_t type, req_id, tx_id, len; };

/* Send one request, wait up to 5s for a reply header. */
static void probe(const char *what, uint32_t type, const char *pl, uint32_t len)
{
struct sockaddr_un a = { .sun_family = AF_UNIX };
struct xsd_sockmsg m = { type, 0xC0DE, 0, len }, r;
struct pollfd p;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);

strcpy(a.sun_path, "/var/run/xenstored/socket");
if (connect(fd, (struct sockaddr *)&a, sizeof(a))) { perror("connect"); return; }

if (write(fd, &m, sizeof(m)) != sizeof(m)) { perror("write"); return; }
if (len && write(fd, pl, len) != (ssize_t)len) { perror("write"); return; }

p.fd = fd; p.events = POLLIN;
if (poll(&p, 1, 5000) == 0)
printf("%-42s -> NO REPLY (5s timeout)\n", what);
else if (read(fd, &r, sizeof(r)) == sizeof(r))
printf("%-42s -> reply type=%u len=%u\n", what, r.type, r.len);
close(fd);
}

int main(void)
{
probe("XS_READ(2)      len=21  (sanity)", 2, "/local/domain/0/name", 21);
probe("XS_GET_QUOTA(25) len=2  (unknown op)", 25, "0\0", 2);
probe("XS_READ(2)      len=0   (known op!)", 2, NULL, 0);
probe("XS_DIRECTORY(1) len=0   (known op!)", 1, NULL, 0);
probe("XS_GET_FEATURE(23) len=0  (xl info -x)", 23, NULL, 0);
probe("XS_GET_QUOTA(25) len=0  (xl list -l)", 25, NULL, 0);
return 0;
}


 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.