Looking for help: Resolve linking issue with ocaml-argon2

Heya!

There is an open pull request for an update to the argon2 bindings for Ocaml. This library is important for web developers, as it’s one of few secure password hashing libraries we have.

This is the pull request:

There seems to be some kind of linking issue that affects not only holds back the release of the updated package but also for it to work with utop.

I can’t really figure out the issue myself and the author of the library does not have time to maintain it, so I’m asking here if anyone is able to look at it and resolve it. I think it’d be big for the ecosystem.

Depending on your needs, you might be able to use the ocaml bindings to libsodium instead. From a quick search they only expose argon2i, so there would still be some value in updating the other bindings, but it could unblock you for now.

Not a full solution, but here’s how I debugged this and a way to fix this:

First, you can repro the issue with just dune runtest in argon2 itself without fpauth. The opam file does not run the tests through opam at the moment.

This library uses ctypes.foreign which means it relies on dlsym to look for symbols. Since it does not use a ~from argument, it is just going to look in the current adress space for the symbol argon2_type2string. This means that all executables using this library need to be linked against -largon2 (the native library).

In src/dune, you can see that there’s some work done to pass the right flags when building the library. And indeed, it can be found in the output of ocamlobjinfo _build/install/default/lib/argon2/argon2.cmxa:

Extra C object files: -largon2 -lrt -ldl

But for some reason, when linking an executable against this library, libargon2 is not present (see the output of ldd _build/default/tests/generic.exe.

Note that if you pass the same flags to the tests, they’ll run normally.

I think what’s happening is that your linker is configured to not link unused libraries. Nothing in the executable statically refers to any symbol in libargon2, so it won’t link it. You can control that by passing --no-as-needed to the linker; so -Wl,--no-as-needed to the compiler; so -ccopt -Wl,--no-as-needed to the ocaml compiler.

Apply this patch:

diff --git a/src/config/discover.ml b/src/config/discover.ml
index 62d2aba..b71bec1 100644
--- a/src/config/discover.ml
+++ b/src/config/discover.ml
@@ -24,5 +24,6 @@ let () =
             | Some deps -> deps)
       in
 
+      (["-ccopt"; "-Wl,--no-as-needed"])@
       concat_map (fun flag -> [ "-cclib"; flag ]) conf.libs
       |> C.Flags.write_sexp "flags.sexp")

And the tests run.

Now, this isn’t a great fix because it requires your linker to know about this option, and it’s only necessary in some cases. I don’t know how to write that in a portable way.

  • An alternative is not to use ctypes.foreign. You can use ctypes stub generation for this. It will generate OCaml and C code that will be linked to your application. Dune has some support for this.
  • You might be able to force linking by refering to a C symbol using external. I’m not sure if it will be stripped or not. This is technically a naked pointer, but if you don’t use it, maybe that’s fine?
1 Like

As hinted by this previous post, it seems that setting a dune flag on your binary has a useful effect.

In my case, I was starting my web server via a dumb binary that simply calls into my internal web library.

The undefined symbol error disappears for me with this extra flag.

$ tail -n+1 *
==> dune <==
(executable
 (name main)
 (flags :standard -cclib -largon2)
 (libraries web))

==> main.ml <==
let () = Web.start ()

The -ccopt "-Wl,--no-as-needed" fix worked on linux but not on mac

(cd _build/default && /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/bin/ocamlopt.opt -w @1..3@5..28@30..39@43@46..47@49..57@61..62-40 -strict-sequence -strict-formats -short-paths -keep-locs -g -o tests/argon2id.exe /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/result/result.cmxa /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/stdlib-shims/stdlib_shims.cmxa /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/integers/integers.cmxa -I /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/integers /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/bigarray-compat/bigarray_compat.cmxa /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ctypes/ctypes.cmxa -I /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ctypes /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ocaml/unix.cmxa -I /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ocaml /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ocaml/threads/threads.cmxa -I /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ocaml /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ctypes-foreign/ctypes_foreign.cmxa -I /Users/runner/work/ocaml-argon2/ocaml-argon2/_opam/lib/ctypes-foreign src/argon2.cmxa tests/.argon2d.eobjs/native/dune__exe.cmx tests/.argon2d.eobjs/native/dune__exe__Argon2id.cmx)
ld: unknown options: --no-as-needed 
clang: error: linker command failed with exit code 1 (use -v to see invocation)
File "caml_startup", line 1:
Error: Error during linking (exit code 1)
File "tests/dune", line 2, characters 16-23:
2 |  (names argon2d argon2i argon2id generic)
                    ^^^^^^^

I’ve posted a summary of the alternatives on this dune issue. I think the takeaway is really to avoid ctypes.foreign for things that are not dynamic loading (plugins).

1 Like

(post deleted by author)