Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

First draft to add capabilities on cstruct #237

Merged
merged 20 commits into from Apr 15, 2019

Conversation

Projects
None yet
7 participants
@dinosaure
Copy link
Member

commented Mar 8, 2019

Let's talk about capabilities under the sun with beers 馃憤

@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Mar 8, 2019

This is a big change, we need to upstream changes on the new cstruct_core.ml file but we can talk about the idea under this PR first.

@avsm

This comment has been minimized.

Copy link
Member

commented Mar 8, 2019

interesting use of object types here! I particularly like the 'a align.

cc @stedolan as he was interested in interfaces for the new effects IO library

@dinosaure dinosaure force-pushed the dinosaure:capabilities branch from 28bdc45 to 9329cd4 Mar 8, 2019

@hannesm

This comment has been minimized.

Copy link
Member

commented Mar 20, 2019

Thanks for opening this PR. While working on the network API, and protocol parsers, I really like the idea to have capabilities/properties of buffers:

  • read-only - for parsing: a received buffer should be marked read-only, and passed on further
  • write-only - not yet sure whether this is really useful - I fail to find a use-case -- checksum computations (which require to read sth) on IP/TCP/UDP get in the way of marking a buffer write-only
  • copy-on-reference - something we discovered in the ixy work and in mirage-net-xen recently: here's a buffer, passed down the stack, but don't hold any reference to it (it's owned by someone else and will be zeroed out / reused at a later point)
    • here @gasche outlined an approach to pass a buffer and abstract token (e.g. timestamp), keep the token in the buffer itself as well, and check on every access that the token matches -- if someone preserved a reference with an old token, attempting to access a buffer will lead to an error
    • look at the mirage-net API atm: in write, a buffer is allocated by the NIC, and filled by the network stack. any malicious stack could keep references to the buffer and read it later (see mirage/mirage-net-xen#83 (review)) (/cc @talex5)

now, reading up on related discussions, as well as face-to-face discussions, I'm wondering:

  • since OCaml 4.06.0, the string type is immutable
  • since OCaml 4.08.0, reading & writing numbers is supported
  • the copy-on-reference seems to be complex (and computationally expensive), or require a linear type system -- which we don't have, a conservative solution is to not hand out references to shared, reused memory, and instead do a copy (solo5 does this e.g.)
  • memory pressure in respect to bigarrays (see this comment)

out of these reasons, I'm curious what you think about the following proposal:

  • get rid of cstruct, replace with bytes for sending / string (unfortunately this PR got merged into 4.08.0 (without get functions for string, but we can easily develop a library that provides these functions) for receiving data?
  • at the OS boundary (i.e. mirage-net-xen), allocate bytes, and blit them after fill into the shared memory region --> less brittle, the stack will never see a reference to the shared memory
  • for C-FFI, as long as external is marked with noalloc, strings/bytes are passed directly (and are not copied to the external heap) AFAIK (please correct me if I'm wrong)

I'm really interested to hear concerns if any, and will attempt to develop a patch to cstruct which uses bytes instead of bigarray -- and see what breaks, and how/whether this improves/degrades performance (any suggestion what an appropriate test would be, otherwise I'd use iperf!?, maybe over TLS (which allocates lots of small cstruct)). I remember having talked about that topic with @samoht who suggested as well to develop a cstruct-backed-by-string/bytes back then, but AFAIK nobody did such an experiment (if someone did, please tell).

@talex5

This comment has been minimized.

Copy link
Contributor

commented Mar 20, 2019

Another thing that would be useful (given the lack of linear types in OCaml) is a way to revoke a bigarray, so that all further attempts to use cstructs based on it fail at runtime. I made a PR for this a few years ago (ocaml/ocaml#389) but it seems that it would prevent some compiler optimisations.

@gasche wrote there:

Another route to take would be to declare Bigarray unfit to represent raw memory, and develop an alternative library to do what people have been doing with Bigarrays instead. This alternative should probably be in stdlib for people to actually accept to move to it. But if we have no one willing to do the work of organizing this (which would have a fairly invasive impact on a lot of third-party libraries), I personally think that giving up on the never-freed guarantee is the best move.

If we had a way to revoke a bigarray, we'd probably also want a capability that allowed the holder of a cstruct to get access to the underlying bigarray, so that e.g. mirage-net-xen could prevent its users from extracting the bigarray and taking a new reference to it. Currently, anyone with a cstruct can do that.

@avsm

This comment has been minimized.

Copy link
Member

commented Mar 20, 2019

@talex5 wrote:

Another thing that would be useful (given the lack of linear types in OCaml)

My understanding is that it's possible to use GADTs to enforce some linearity, but at the cost of making the API difficult to use since it 'taints' all functions that use it since they have to unpack the GADT to get at any state.

However, one observation I have is that linearity is normally done the wrong way around -- it is embedded in the middle of an application and so hard to enforce. We could try to do things in reverse: a Mirage scheduler that invokes the application code initialises the linearity state and calls the IO functions. If a function needs to be non-linear for a while (i.e. weaken the guarantee) then it can do a shift to GC/reference counting, and then when that is done we move back to a linearly tracked dataflow.

The advantage of this model is that for a network stack, we should never need the non-linear access until we get to the application layer. While linearity is present, is it easy to have a non-copying slicing API. At the application layer, we can take a decision on how to shift to non-linearity -- e.g. by copying to bytes/string (as suggested by @hannesm above), or doing fancy paging tricks. I believe this model would also satisfy @talex5's suggestion of bigarray lifetime management since the outer layer would be tracking lifetimes of IO frames explicitly (from the xen driver up).

This is quite a big change, but I believe that putting linearity in the right place in the IO stack is what's missing to have a reasonable lifetime tracking model in the current setup.

@hannesm

This comment has been minimized.

Copy link
Member

commented Mar 20, 2019

@talex5 ah, thanks for that pointer. my TL;DR there is: we should develop a "safe" library that contains a free, evaluate it, and if it meets our demands, upstream it into the OCaml runtime. I've also heard there's some interest in an immutable bigarray by some compiler folks. My motivation to go this way is not too high, since I suspect that moving to String/Bytes will give us the same benefits. This shouldn't prevent anyone else from working on such a PR, and evaluating it.

@avsm writes:

However, one observation I have is that linearity is normally done the wrong way around -- it is embedded in the middle of an application and so hard to enforce. We could try to do things in reverse: a Mirage scheduler that invokes the application code initialises the linearity state and calls the IO functions. If a function needs to be non-linear for a while (i.e. weaken the guarantee) then it can do a shift to GC/reference counting, and then when that is done we move back to a linearly tracked dataflow.

I don't fully understand this proposal:

  • What is this MirageOS scheduler? Where does it live? Does it own the memory?
  • "If a function needs to be non-linear for a while" <- is this something inferred / automatic, or manual function calls?

TL;DR: IMHO we should for a next cstruct major version have a good story about safety with the different scenarios outlined above. Performance optimization to reduce copying is something we can do in a later step (after evaluating the different proposals). My current intuition is that copying into the OCaml heap for received buffers is fine (and hardly measurable).

Of course, if we can smoothly integrate a linearity invariant, we won't need to copy stuff around. If we fail to do so, better copy than leak information ;)

@avsm

This comment has been minimized.

Copy link
Member

commented Mar 20, 2019

Consider the ownership of a buffer. When it comes in from the outside world, there is precisely one owner (the 'hardware' source it came from). Then we immediately lose this guarantee by copying it into the OCaml heap. I'm proposing that rather than passing the single-ownership buffer into a multi-ownership world, we maintain that linearity that we started with as long as possible. This requires plumbing through a 'linear state' value (a GADT probably) through all calls that use a buffer. Constructing this state value has to be done by something that initialises the virtual hardware.

Note that this isn't a concrete interface proposal: I need to defer to @yallop's expertise in this matter as he has supervised a student project that worked in this area last year.

@hannesm:

Performance optimization to reduce copying is something we can do in a later step (after evaluating the different proposals). My current intuition is that copying into the OCaml heap for received buffers is fine (and hardly measurable).

Unfortunately it is pretty measurable when you have lots and lots of buffers being sliced up and passed around. The most obvious example is Cohttp vs httpaf at the application level, but further compounded in the tcp/ip layers.

Linearity is a really good approach if we can get it right -- it satisfies both performance and safety needs by taking advantage of a vital single-ownership invariant that is almost always a bug if violated dynamically.

@gasche

This comment has been minimized.

Copy link
Contributor

commented Mar 20, 2019

Linearity can be enforced at runtime by adding an indirection: instead of passing "the buffer" as an OCaml value, the values you share/copy are "a mutable pointer to the buffer". Whenever you decide to give up ownership of the buffer to someone else, they create their own mutable pointer and you rewrite your own pointer to NULL, so that any of your future accesses to the buffer will fail.

(This assumes that there is no OCaml function to just dereference those pointers and return the "raw buffer" as an OCaml value; otherwise that value can be copied/leaked, the cat is out of the bag. But it's actually sort of easy to guarantee this with "buffer of bytes", as you can just provide abstract get/set/read/write/sub/blit functions.)

I am not familiar with the Mirage codebase, but my gut feeling is that untying your core data structures from "bigarrays as they are in the standard library" would help quite a bit with experimenting with other approaches and seeing what works. If it was my project (it isn't!), I would try to make the types of data blobs more abstract to make such a transition possible.

@avsm

This comment has been minimized.

Copy link
Member

commented Mar 20, 2019

@gasche wrote:

I am not familiar with the Mirage codebase, but my gut feeling is that untying your core data structures from "bigarrays as they are in the standard library" would help quite a bit with experimenting with other approaches and seeing what works.

This is right, but the devil's in the performance detail :-) The introduction of the bigarray primitives in ocaml/ocaml#5771 means that it's hard to move to a separate library that wraps allocated C buffers but that can still efficiently unmarshal values to OCaml. We either have to use a string, or ensure that the offsets within the C malloced memory are the same as a bigarray representation.

If there's an effective workaround for that, or perhaps a fix in the compiler to add support for unmarshalling from arbitrary memory regions that were externally allocated, then I would be heavily in favour of experimenting with new memory representations for Mirage IO. I don't think that either string or bigarray are satisfactory for dealing with heavy IO, since they were both designed for other purposes.

@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Mar 20, 2019

About experimentation, I did a big work between bytes/string/bigarray with GADT, functor and abstract type here: https://github.com/dinosaure/buffet/tree/master/lib

From some discussions with @chambart, the best trade-off (performance/readability) to be able to be abstracted over a buffer is the functor way. But I don't have a benchmark on a large application (such as ocaml-git) to say: this way is the best one.

With @Drup, it seems than about question of abstraction, the best is to provide a common interface and let the end-user to use specific implementation according to this interface.

About linearity, this problem seems to be common (specially for ocaml-git). A runtime-check seems to be reasonable but I'm afraid about the failure case where it can appear from anywhere without any context. I mean, with ocaml-git, we have this kind of problem and the hard time is when we try to find where we share the buffer (and where we should not). Bug pattern of a shared and compromised buffer is not so difficult to be understandable.

It's why I would like a static analyze about that instead a runtime-check.

About the choice of bigarray instead bytes (or vice-versa), clearly I'm not sure if it's really interesting. Indeed, in some situations, to use bytes instead bigarray can be faster in certain contexts but it's not true for all cases. digestif provides digest functions for bytes, string and bigarray and, for specific applications, bigarray is better than bytes, specially when bigarray never move in the heap (so we are able to do some parallelization stuff on it easily). So the choice really depends on what we want and it's why I prefer an common API with cast operations between each kind of buffers.

This is my 2 cts 馃憤 .

@gasche

This comment has been minimized.

Copy link
Contributor

commented Mar 20, 2019

About linearity, this problem seems to be common (specially for ocaml-git). A runtime-check seems to be reasonable but I'm afraid about the failure case where it can appear from anywhere without any context. I mean, with ocaml-git, we have this kind of problem and the hard time is when we try to find where we share the buffer (and where we should not). Bug pattern of a shared and compromised buffer is not so difficult to be understandable.

It's why I would like a static analyze about that instead a runtime-check.

If you use dynamic enforcement of unique ownership, I think it wouldn't be too hard to have a debug mode that helps you find where the issue come from. As an example example, you could have an opt-in mode where, when you transfor ownership to someone else, you could capture a stack trace and place that trace in the "there is no buffer anymore" structure (instead of just NULL). If a later access fails because the buffer is gone, at that point you can then display the saved trace that pinpoints where exactly the value was given to someone else.

@avsm: the primtives are a good point. Two vague ideas coming to mind:

  • You could still use other structures for experiments, and compare with "bigarrays without the primitives" to get a fair performance comparison (everything would be appropriately slower).
  • If you wanted an efficient out-of-heap buffer as a library, instead of performing a C call, you could write the access primitives as functions in assembly code that follows the OCaml calling convention and link to that. (This is not exactly as cheap as an inline read/write instruction, but it should be pretty good). (This is a hacky idea and I'm sure you would find people excited to experiment with that.)
@talex5

This comment has been minimized.

Copy link
Contributor

commented Mar 21, 2019

write-only - not yet sure whether this is really useful - I fail to find a use-case -- checksum computations (which require to read sth) on IP/TCP/UDP get in the way of marking a buffer write-only

write-only would be useful if the firewall was going to be updated to use the new mirage-net-xen. Since 1.10, mirage-net-xen is giving out a cstruct that is under the control of the remote (malicious) domain, and can therefore change at any time. If the firewall reads out any data that it has just written and depends on it any way, we likely have a security vulnerability. Linear types don't help here because we don't control the other domain.

( For example, if the firewall decided to route a packet to a client app VM, wrote the packet to the shared ring, and then looked at the written packet to generate the NAT rules for the return path. The app VM could change the source or destination address and thus add a bogus rule to the NAT. A bit far-fetched I guess, but I'd rather not have to worry about such things. )

@hannesm

This comment has been minimized.

Copy link
Member

commented Mar 21, 2019

@talex5 good point. so the firewall read a packet from some shared memory, processes the data (which includes in the NAT case some rewriting, and checksum re-computation), and writes it to another region of shared memory. IIRC the current API in our network stack writes (e.g. TCP and IP data) to the buffer, and then computes the checksum using the very same buffer, which in your shown firewall case should not be done (but instead, some temporary scratch buffer for checksum computation should be used).

@gasche / @dinosaure / @avsm I'd really like to avoid functorising over Cstruct/buffer all of our libraries. the way forward sounds like finding a common interface of different implementations (as @dinosaure pointed to his s.mli) -- which does not expose the type t -- upstream the interface, fix users of Cstruct that rely on internals, and then we can play around with different implementations. WDYT?

@talex5

This comment has been minimized.

Copy link
Contributor

commented Mar 21, 2019

@hannesm yes, although in the case the checksum it doesn't really matter. It's a bit strange that the destination can cause us to calculate a checksum for something other than the packet we intended to send, but the only thing we do with the checksum is to send it to the same destination anyway.

@emillon

This comment has been minimized.

Copy link
Contributor

commented Mar 21, 2019

Regarding capabilities, I agree that it's something that's missing in the ecosystem. Having immutable binary data is super useful for things like crypto code.

I think it would be easier to integrate if this was a new library implemented on top of cstruct, though (rather than changing cstruct). That way we could incrementally adapt (some) cstruct users to that.

Regarding using strings vs bigarrays, yes using malloc is probably wasting many cycles in mirage at the moment, but brings the important property that data does not move. The arguments I've heard in favor of that include that it makes it possible to:

  • erase secrets (not done in practice, probably difficult to do at scale, and only brings value when uninitialized data is exposed to app code)
  • have page-aligned buffers (from what I understand, it was more important in earlier version of mirage than it is now)
@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Mar 21, 2019

I think it would be easier to integrate if this was a new library implemented on top of cstruct, though (rather than changing cstruct). That way we could incrementally adapt (some) cstruct users to that.

In the details, this PR does not change cstruct nor implementation. Implementation of Cstruct_cap is only a weird include of cstruct_core/cstruct and I just put a cstruct_cap.mli.

About linearity, of course, we need to change Cstruct.t to put a private token as @gasche said.

@emillon

This comment has been minimized.

Copy link
Contributor

commented Mar 21, 2019

Put it differently, could this be a separate project, or does it require access to cstruct's private API?

@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Mar 21, 2019

It's more about maintainability of this kind of package where, for any update of cstruct, we should update cstruct-with-cap too (where they should share the same implementation). Then, I need to investigate but I suspect a regression performance in this way (but may be I'm wrong in favor to cross-optimization of *.cmx{a}.

I prefer this way where we completely keep the compatibility with the current status of cstruct and provide a sub-module which integrate capabilities (and ensure than we keep exactly the same semantic of functions) - and by this way, avoid a duplication of packages.

@hannesm

This comment has been minimized.

Copy link
Member

commented Mar 21, 2019

I agree with @dinosaure in the (tentative) migration plan:

  • provide an API with capabilities in version X
  • modify users to the new API
  • drop old API in version Y

Now, to start the migration process, the first step is to provide a capability-API, rather sooner than later. Removal of some internals from the API to allow non-Bigarray-based implementations is orthogonal to that AFAICT.

Your proposal of a separate opam package adds more complexity: we'd need to add it as dependency to all users, and drop the cstruct package entirely once the migration is done (or should the other opam package be renamed to cstruct, and require all users to change again?).

I don't understand your "erasing secrets" argument, you're aware that Cstruct.create_unsafe is around (also Bigarray.Array0.create)? In my perspective, "erasing secrets" is still a long way to go in MirageOS (and likely requires OCaml runtime changes -- if you're interested, you may enjoy reading https://blogs.akamai.com/2014/04/heartbleed-update-v3.html - which makes clear that you need to protect not only the raw key material, but also lots of derived material). FWIW, cstruct does not deal with page alignment.

@emillon

This comment has been minimized.

Copy link
Contributor

commented Mar 21, 2019

Oh, erasing secrets is definitely not my argument - only something that I've heard several times and that was missing here for completeness. I agree that it's not the important part to fix right now.

FWIW, cstruct does not deal with page alignment.

But io-page relies on cstruct to do that at the moment, right? I don't know how important that library is, but it wouldn't work with strings.

@dinosaure dinosaure force-pushed the dinosaure:capabilities branch from 9329cd4 to e44a574 Mar 25, 2019

@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Mar 25, 2019

PR rebased on master, if we can start to talk about details (mostly on cstruct_cap.mli) 馃憤 !

@hannesm
Copy link
Member

left a comment

in general, I like this very much. thanks for your work on this! some comments above.

all functions that have two (or more) int arguments: can we please label them (?off and ?len)?

I'm not sure about the allocator argument -- is anyone actually using this? if not, for simplicity we can remove it now, and later add it if we need it.

the copy is superfluous once to_string/to_bytes accept ?off:int and ?len:int.

Show resolved Hide resolved lib/cstruct_cap.mli Outdated
Show resolved Hide resolved lib/cstruct_cap.mli
Show resolved Hide resolved lib/cstruct_cap.mli Outdated
Show resolved Hide resolved lib/cstruct_cap.mli Outdated
Show resolved Hide resolved lib/cstruct_cap.mli Outdated
Show resolved Hide resolved lib/cstruct_cap.mli Outdated
Show resolved Hide resolved lib/cstruct_cap.mli

@hannesm hannesm referenced this pull request Mar 25, 2019

Merged

Fixes for #244 #245

@dinosaure dinosaure force-pushed the dinosaure:capabilities branch from 2d3d109 to d433d1c Mar 28, 2019

@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Mar 28, 2019

PR rebased, waiting #245 and about ?allocator argument, I would like more feedback about that. Then, I will make documentation on cstruct_cap.mli in an other commit.

@hannesm

This comment has been minimized.

Copy link
Member

commented Mar 29, 2019

about ?allocator: I've looked through my cloned repositories, and there is not a single use thereof -- i.e. for simplicity, I'd remove it from the new interface. If there's demand at a later point, we can always re-introduce such an argument. It's also not clear to me what its semantics should be, given that t is now abstract.

@dinosaure dinosaure force-pushed the dinosaure:capabilities branch from e6095bd to 1df2c87 Apr 9, 2019

@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Apr 9, 2019

Rebased 馃憤

@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Apr 10, 2019

Documentation was done. I don't want to push the merge button but it's ready to merge I believe.

Show resolved Hide resolved lib/cstruct_cap.ml Outdated
@hannesm

This comment has been minimized.

Copy link
Member

commented Apr 10, 2019

thanks for the documentation. imho this is good now API-wise. it would be great to have some test cases (likely you can copy and modify some Cstruct tests for Cstruct_cap)!?

Show resolved Hide resolved lib/cstruct_cap.ml
Show resolved Hide resolved lib/cstruct_cap.mli Outdated
@dinosaure

This comment has been minimized.

Copy link
Member Author

commented Apr 15, 2019

Who wants to push merge button 馃槇 ?

@avsm

This comment has been minimized.

Copy link
Member

commented Apr 15, 2019

I've got some fixes to the ocamldoc to push and then i'll merge

@avsm

This comment has been minimized.

Copy link
Member

commented Apr 15, 2019

Hm, the wrapping makes the Cstruct.Cap odoc look absolutely terrible :-/

I'm tempted to unwrap this but make Cstruct_cap and Cstruct_core into private modules so their cmis arent exposed.

avsm added some commits Apr 15, 2019

just expose Cstruct_cap as a toplevel interface
This removes the various module aliases that make the documentation
hard to read and also the interdependency between Cstruct and
Cstruct_cap which isnt necessary
@avsm

This comment has been minimized.

Copy link
Member

commented Apr 15, 2019

Good to merge after CI I think. This has now three toplevel modules: Cstruct, Cstruct_cap and Cstruct_core. Those are unlikely to module clash, and the docs are neater and there is no dependency between Cstruct and Cstruct_cap.

@avsm avsm merged commit 038b7d8 into mirage:master Apr 15, 2019

2 checks passed

continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details

This was referenced Apr 15, 2019

avsm added a commit to avsm/opam-repository that referenced this pull request Apr 19, 2019

[new release] cstruct-async, ppx_cstruct, cstruct, cstruct-unix, cstr鈥
鈥ct-sexp and cstruct-lwt (5.0.0)

CHANGES:

**Security**: This release tightens bounds checks to ensure
that data outside a given view (but still inside the underlying
buffer) cannot be accessed.

- `sub` does more checks (mirage/ocaml-cstruct#244 mirage/ocaml-cstruct#245 @hannesm @talex5 review by @dinosaure)
- `add_len` and `set_len` are now deprecated and will be removed
  in a future release. (mirage/ocaml-cstruct#251 @hannesm)
- do not add user-provided data for bounds checks
  (mirage/ocaml-cstruct#253 @hannesm, report and review by @talex5)
- improve CI to add fuzzing (mirage/ocaml-cstruct#255 mirage/ocaml-cstruct#252 @avsm @yomimono @talex5)

**Remove Unix dependency**: cstruct now uses the new `bigarray-compat`
library instead of Bigarray directly, to avoid a dependency on Unix
when using OCaml compilers less than 4.06.0.  This will break downstream
libraries that do not have a direct dependency on `Bigarray`.  Simply
fix it in your library by adding a `bigarray` dependency in your dune
file. (mirage/ocaml-cstruct#247 @TheLortex)

**Capability module**: To improve the safety of future code with stronger type
checking, this release introduces a new `Cstruct_cap` module which makes the
underlying Cstruct an abstract type instead of a record. In return for this
extra abstraction, the module can enforce read-only, write only, and read/write
buffers by tracking them as phantom type variables.  Although this library
shares an implementation internally with classic `Cstruct`, it is a significant
revision and so we will be gradually migrating to it.  Feedback on it is
welcome! (mirage/ocaml-cstruct#237 @dinosaure and many excited reviewers)

**Ppx compare functions**: A new `compare_X` function is generated for
`cenum` declarations. This respects custom ids supplied in the cenum
declaration and so is more robust than polymorphic compare (mirage/ocaml-cstruct#248 @emillon)

The CI has also been switched over to both Azure Pipelines and Drone in
addition to Travis, and as a result the tests all run on Windows, macOS,
various Linux distributions, on x86 and arm64 machines, and runs AFL
fuzz tests on the Drone cloud (mirage/ocaml-cstruct#255 @avsm).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can鈥檛 perform that action at this time.