-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Audit stdlib for mutable state #10960
Comments
I moved this issue from the Multicore repository so that we may resume work on this now that Multicore was merged onto trunk.
I think maintainers can edit the initial post, so we can try to keep the table up to date if this sounds reasonable? |
My memory of the last meeting for |
@Octachron Indeed, I updated this entry to reflect the notes from WG 4, thanks. I made a first run through the list today and filled a few empty rows. I also removed a few lines related to module that were removed since the initial writing of this list. Tomorrow I will take a stab at more unknown items on the list (there are bigger fishes like For the |
Printf and Format use different output engines, and I only discussed Format. (For Format, there is a performance-related work item in #10876 that is just right for @Octachron to handle once the 4.14 release becomes less interesting.) I just looked, there is no mutable state in Printf (or really CamlinternalFormat). Of course the default formatters (printf, eprintf) use
My understanding is that those operations are protected by per-channel mutexes, so multicore usage is safe, but the output will be mangled if several domains try to print simultaneously. The current approach of Format provides better printing guarantees, as interleavings only happen on flush points. We could also implement it for channels, but it would also be expensive. (It would be nice if we could at least leave enough control to |
There's the Implementation-wise, it means using recursive locks to protect I/O channels, something that I considered in the past and that would be easy to implement. |
Something like:
that can be used like this:
|
I think it would be nice to try this. If it's not too expensive, it could provide much better printing behavior when using On the other hand, exposing the locking function to user programs is probably a bad idea, because it locks the whole domain, and we promised not to do that (remember, this is the reason why we forbid concurrent Lazy.force). I guess the current mantra is that users should use a higher-level library that provides asynchronous IO. |
On the subject of |
Personally I find the global-lock version more tempting, because I don't want to have to wonder about which domain I'm in when I write code. Plus it simplifies the documentation -- nothing has to be said. This said, I have very little experience using Dynlink. |
Concerning Dynlink, I guess the functions would be safe (= not crash the program) if protected by a global lock. Some global OCaml tables are modified, e.g. Symtable in bytecode, and then a code fragment is registered with the runtime (using a lock-free skiplist!). All in all, it looks like this can be done from any domain, in mutual exclusion. However, the API remains dangerous if it's used by two domains (or two threads) concurrently, as one could do |
Concurrent write to the list of |
Yes, the API is already questionable in the presence of threads. |
Hi everyone, I spent some time reviewing a few Stdlib modules lately ( Needs fixing, actionable
Unclear if action required
Needs fixing, and possibly hints at bigger problemsThe This is probably OK in 4.x, but for multicore |
Thank you @OlivierNicole for your investigations! I updated the table accordingly to your findings. |
Thanks for the careful reading. I agree that |
Taking in account #11171, #11193, #11225, #11227 and #11228, I am left with the following items for the stdlib audit: Changes required:
Documentation:
Did I miss anything? |
For non-float-arrays, individual reads and writes respect the memory model; memory safety is preserved and there are no crashes with racy access. For float arrays, operations do not respect the OCaml memory model. Tearing is possible. However, since float arrays do not contain pointers, there is no possibility of crashes. For context, see ocaml-multicore/ocaml-multicore#822.
I'd mark ephemerons as not concurrency-safe, and users must implement their own synchronization. The proposed new API has been included, but the necessary implementation can come after 5.0. For context, see #10737. |
Ephemerons weak hash tables are documented as concurrency-unsafe in #11193. |
Yes, for all operations except blit. For float arrays, we use memmove for blit, which doesn’t guarantee aligned, word-sized writes; memmove may choose to do byte-by-byte copy of values. Hence, tearing may happen with blit even on architectures where word size and double size agree.
Yes, indeed.
This sounds ok. The fact that program doesn’t crash with data races is the baseline expectation and I guess there’s no point explicitly mentioning that for array operations. |
I had forgotten that the |
I am looking at I can take care of |
In case of concurrent calls to ocaml/otherlibs/unix/channels_win32.c Lines 63 to 73 in 09ed8df
What do we do for racy accesses in C code? Do we treat it as UB? |
Well spotted! I'm not sure we have a systematic doctrine to deal with these races, but in this particular case it looks like a perfect use case for a compare-and-swap, solving the race condition for a very low cost. |
Follow-up PR: #11304. |
Summarizing the current state of the audit: we still have to integrate the documentation before the final release, Concerning the internal modules The runtime unloading has not been audited as far as I know (beyond the issue with Documentation:
Required fixes
|
I finished reviewing
Remaining points for which I have not opened tickets (cc @Engil):
(Edit: improved readability, removed unrelated PRs) |
Regarding OO programming, in my understanding when calling a method, the method address is resolved and possibly cached by modifying the bytecode: Line 1224 in 7412593
Isn't this a data race in case of multiple domains executing the same |
As far as I can tell the same is true in native code, where instead of a word in the bytecode a global method cache is mutated. Edit: Thinking again, as in OCaml data races are locally bound and here the absence of sequential consistency does not matter (as the cached value is checked before use), we are probably safe in native code. |
This is a cache: any integer value in the correct range is a safe value, although better performance is achieved if the value is the one last written. So, this is a benign race condition. For the bytecode interpreter, we'd better inform the C compiler of this fact, e.g. with atomic relaxed accesses? For the ocamlopt-generated code, no change is required. |
Proposed change for the bytecode interpreter as #11512. |
With the last buffer change, I propose to close this meta-issue PR . The remaining open questions for OCaml 5.1 would probably be better served by fresh issue. |
This is moved from ocaml-multicore/ocaml-multicore#757
This issue tracks the status of auditing stdlib for mutable state. OCaml 5.00 stdlib will have the following guarantees:
There are two categories by which we classify the stdlib modules.
Top-level state
The module has some top-level mutable state that may cause surprising behaviours. For example, Arg module has a top-level mutable ref called
current
that represents the cursor of theSys.argv
argument being parsed. If two domains concurrently use Arg module, then they may see arguments being skipped. These cases either need to be:Filename.temp_file
, Format module for predefined buffers) orArg
module; it is reasonable to expect only the main domain to parse command-line arguments).Mutable interface
The module may create mutable state and return it. For example, Queue, Stack, Hashtbl, etc. These modules will be left as sequential only and not thread-safe. Multiple concurrent invocations may lead to non-linearizable behaviours. We leave it to the user to put a mutex around the use of the mutable structure (or use thread-safe libraries such as
domainslib
).Not all mutable interfaces are unsafe. For example, concurrent array get and set are fine. But we still mark the array for mutable interface. The reason is that, we also use mutable interface to indicate cases where the observed concurrent behaviour cannot be explained by assuming that each API call to the module executes atomically (linearizability). For example, though an individual get and set of Array fields is safe, we mark it as mutable interface as iteration functions that modify the same array may leave the array in a state that cannot be explained by linearizability.
Stdlib modules
The column "needs work" tracks whether code changes need to be made for OCaml 5.00 MVP.
Needs work column will be
N
if the work has already been done. For example, the Format module has top-level mutable state, which has been made domain-safe already in the Multicore OCaml 5.00 branch. Another example is Printexc, which has been made thread-safe in OCaml trunk in a forward-compatible manner with multicore.Needs work does not encompass documentation; Needs work may be
N
and documentation may need to be updated.current
type buffer
Obj.set_field
,Lazy.force
current_temp_dir_name
and setter getter functionsalarm
may need attention.caml_gc_get/set
may need attentionrandomized
?input_all
will likely not return contiguous chunks. (already an issue on4.X
)current_lookahead_fun
and a few effectful functions.Stdlib
raw_backtrace_entries
which returns an array (which could be modified concurrently). set_uncaught_exception needs to be checked (uses a global reference)caml_shutdown
as called byStdlib.exit
may access a non-atomicstartup_count
variable. This may cause issues under Multicore. Should be atomic?caml_ml_enable_runtime_warnings
is a global boolean. To be documented or made atomic?otherlibs
The text was updated successfully, but these errors were encountered: