diff --git a/README.md b/README.md index db5f480..af1a98b 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,12 @@ Acton server, to OpenSSH `sshd`, or be driven by the OpenSSH `ssh` client. ## Requirements -This package depends on a fork of libssh that adds the hooks needed for an -external event loop (`ssh_session_handle_poll`, `ssh_channel_read_buffered`, -server build) and builds with Zig. The dependency is declared in -[`Build.act`](Build.act); point it at your checkout of the fork. +This package builds libssh from the unmodified upstream release tarball; the +Zig build wrapper in [`deps/libssh`](deps/libssh) owns the build configuration +and compiles in the hooks needed for an external event loop +(`ssh_session_handle_poll`, `ssh_channel_read_buffered`) as extra sources +([`deps/libssh/src/libssh_acton.c`](deps/libssh/src/libssh_acton.c)) — no fork +of libssh is required. The dependency is declared in [`Build.act`](Build.act). libssh and its crypto backend (mbedtls) run on the C library heap with their own ownership; only libuv and Acton objects live on the GC heap. See the diff --git a/deps/libssh/build.zig b/deps/libssh/build.zig index d796f3f..2d6fff2 100644 --- a/deps/libssh/build.zig +++ b/deps/libssh/build.zig @@ -46,14 +46,18 @@ pub fn build(b: *std.Build) void { }, .{ .PACKAGE = "libssh", - .VERSION = "0.11.0", + .VERSION = "0.12.0", .PROJECT_NAME = "libssh", - .PROJECT_VERSION = "0.11.0", + .PROJECT_VERSION = "0.12.0", .SYSCONFDIR = "/etc", .BINARYDIR = "/usr/bin", .SOURCEDIR = ".", + .GLOBAL_CONF_DIR = "/etc/ssh", + .USR_GLOBAL_CONF_DIR = "/usr/etc/ssh", .GLOBAL_BIND_CONFIG = "/etc/ssh/libssh_server_config", + .USR_GLOBAL_BIND_CONFIG = "/usr/etc/ssh/libssh_server_config", .GLOBAL_CLIENT_CONFIG = "/etc/ssh/ssh_config", + .USR_GLOBAL_CLIENT_CONFIG = "/usr/etc/ssh/ssh_config", .HAVE_ARGP_H = true, .HAVE_ARPA_INET_H = true, .HAVE_IFADDRS_H = (t.os.tag != .windows), @@ -116,6 +120,18 @@ pub fn build(b: *std.Build) void { .HAVE_LIBMBEDCRYPTO = true, .HAVE_PTHREAD = has_pthread, .HAVE_CMOCKA = false, + .HAVE_LIBFIDO2 = false, + + // No native curve25519 in either backend config: use the bundled + // reference implementation (external/curve25519_ref.c + + // curve25519_fallback.c). Likewise ML-KEM uses the bundled + // libcrux code (mlkem_native.c) rather than a crypto backend. + .HAVE_MBEDTLS_CURVE25519 = false, + .HAVE_GCRYPT_CURVE25519 = false, + .HAVE_GCRYPT_MLKEM = false, + .HAVE_OPENSSL_MLKEM = false, + .HAVE_MLKEM1024 = false, + .HAVE_MEMSET_EXPLICIT = false, .HAVE_GCC_THREAD_LOCAL_STORAGE = false, .HAVE_MSC_THREAD_LOCAL_STORAGE = false, @@ -135,6 +151,7 @@ pub fn build(b: *std.Build) void { .HAVE_GCC_BOUNDED_ATTRIBUTE = false, .WITH_GSSAPI = false, .WITH_ZLIB = false, + .WITH_FIDO2 = false, .WITH_SFTP = false, .WITH_SERVER = with_server, .WITH_EXEC = true, @@ -158,7 +175,7 @@ pub fn build(b: *std.Build) void { .include_path = "libssh/libssh_version.h", }, .{ .libssh_VERSION_MAJOR = 0, - .libssh_VERSION_MINOR = 11, + .libssh_VERSION_MINOR = 12, .libssh_VERSION_PATCH = 0, }); @@ -194,6 +211,8 @@ pub fn build(b: *std.Build) void { "src/ecdh.c", "src/error.c", "src/getpass.c", + "src/gzip.c", + "src/hybrid_mlkem.c", "src/init.c", "src/kdf.c", "src/kex.c", @@ -204,6 +223,7 @@ pub fn build(b: *std.Build) void { "src/match.c", "src/messages.c", "src/misc.c", + "src/mlkem.c", "src/options.c", "src/packet.c", "src/packet_cb.c", @@ -211,9 +231,11 @@ pub fn build(b: *std.Build) void { "src/pcap.c", "src/pki.c", "src/pki_container_openssh.c", + "src/pki_context.c", "src/poll.c", "src/session.c", "src/scp.c", + "src/sntrup761.c", "src/socket.c", "src/string.c", "src/threads.c", @@ -259,8 +281,16 @@ pub fn build(b: *std.Build) void { "src/external/chacha.c", "src/external/poly1305.c", "src/chachapoly.c", - "src/external/curve25519_ref.c", + "src/external/sntrup761.c", "src/dh-gex.c", + // curve25519: no backend-native implementation is enabled + // (HAVE_MBEDTLS_CURVE25519=false), so use the bundled reference code. + "src/external/curve25519_ref.c", + "src/curve25519_fallback.c", + // ML-KEM: no backend support (HAVE_MLKEM1024=false), so use the + // bundled libcrux implementation. + "src/mlkem_native.c", + "src/external/libcrux_mlkem768_sha3.c", }) catch unreachable; lib.root_module.addCSourceFiles(.{ @@ -268,6 +298,15 @@ pub fn build(b: *std.Build) void { .files = source_files.items, .flags = flags.items, }); + // Acton's event-loop hooks (ssh_session_handle_poll, + // ssh_channel_read_buffered): new functions on top of the unmodified + // upstream sources, kept local to this wrapper instead of patching the + // tarball. Declarations live in include/libssh/libssh_acton.h. + lib.root_module.addCSourceFiles(.{ + .files = &.{"src/libssh_acton.c"}, + .flags = flags.items, + }); + lib.root_module.addIncludePath(b.path("include")); lib.root_module.addIncludePath(upstream.path("include")); // mbedtls headers only — the objects are linked via Acton's base at the final // executable link (see top-of-file note). acton_sysdeps points at the @@ -279,6 +318,7 @@ pub fn build(b: *std.Build) void { lib.installHeadersDirectory(upstream.path("include/libssh"), "libssh", .{}); lib.installHeader(version_header.getOutputFile(), "libssh/libssh_version.h"); + lib.installHeader(b.path("include/libssh/libssh_acton.h"), "libssh/libssh_acton.h"); b.installArtifact(lib); } diff --git a/deps/libssh/build.zig.zon b/deps/libssh/build.zig.zon index 6d78f2a..c3215fa 100644 --- a/deps/libssh/build.zig.zon +++ b/deps/libssh/build.zig.zon @@ -1,15 +1,16 @@ .{ .name = .libssh, - .version = "0.11.0", + .version = "0.12.0", .fingerprint = 0xda35516053c953ab, .minimum_zig_version = "0.16.0", .dependencies = .{ - // libssh as a pure-source dependency: the actonlang/libssh "acton" - // branch carries no build.zig (only a minimal set of source patches); - // this wrapper owns the build configuration. Pinned by commit. + // libssh as a pure-source dependency: the unmodified upstream release + // tarball. This wrapper owns the build configuration, and the Acton + // event-loop hooks are added as extra sources (src/libssh_acton.c) + // rather than patches, so no fork of libssh is needed. .libssh_upstream = .{ - .url = "https://github.com/actonlang/libssh/archive/82c6ffcea9ab3ae2b61c082d6b6900899af8b655.tar.gz", - .hash = "N-V-__8AAOhETABotmEwFsdxaIh9QrKb55e9pRhi4IOlEugd", + .url = "https://git.libssh.org/projects/libssh.git/snapshot/libssh-0.12.0.tar.gz", + .hash = "N-V-__8AAJQ3YACvCqTRvZk5i48MCnXp0JPImYjVIjw_fEG2", .lazy = false, }, // No mbedtls dependency here: libssh compiles against the toolchain's @@ -19,5 +20,7 @@ .paths = .{ "build.zig", "build.zig.zon", + "include", + "src", }, } diff --git a/deps/libssh/include/libssh/libssh_acton.h b/deps/libssh/include/libssh/libssh_acton.h new file mode 100644 index 0000000..e1c037a --- /dev/null +++ b/deps/libssh/include/libssh/libssh_acton.h @@ -0,0 +1,31 @@ +/* Acton additions to the libssh API. + * + * These functions are NOT part of upstream libssh. They are compiled into + * the library by this wrapper's build.zig (src/libssh_acton.c) on top of the + * unmodified upstream source tarball pinned in build.zig.zon. They provide + * the hooks Acton's external (libuv) event loop needs: + * + * - ssh_session_handle_poll: feed poll(2)-style revents for the session's + * fd into libssh's internal poll machinery, so libssh makes progress + * without owning the event loop. + * - ssh_channel_read_buffered: drain data libssh has already buffered for + * a channel without touching the socket, for callback-driven reads. + */ +#ifndef LIBSSH_ACTON_H +#define LIBSSH_ACTON_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +LIBSSH_API int ssh_session_handle_poll(ssh_session session, int revents); +LIBSSH_API int ssh_channel_read_buffered(ssh_channel channel, void *dest, + uint32_t count, int is_stderr); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBSSH_ACTON_H */ diff --git a/deps/libssh/src/libssh_acton.c b/deps/libssh/src/libssh_acton.c new file mode 100644 index 0000000..1279e5e --- /dev/null +++ b/deps/libssh/src/libssh_acton.c @@ -0,0 +1,219 @@ +/* Acton additions to libssh — see include/libssh/libssh_acton.h. + * + * This file is compiled together with the unmodified upstream libssh source + * tarball (pinned in build.zig.zon); it only uses internals exposed through + * libssh's private headers. The two functions are taken verbatim from the + * actonlang/libssh fork (commits "Expose session poll handler" and "Expose + * ssh_channel_read_buffered for callback-driven reads"). + * + * grow_window() and ssh_channel_has_unread_data() are static helpers inside + * src/channels.c upstream, so they cannot be called from another translation + * unit; the copies below must be kept in sync with the pinned tarball + * (libssh 0.11.0). + */ +#include "config.h" + +#include +#include + +#include "libssh/priv.h" +#include "libssh/libssh.h" +#include "libssh/buffer.h" +#include "libssh/channels.h" +#include "libssh/packet.h" +#include "libssh/poll.h" +#include "libssh/session.h" +#include "libssh/socket.h" +#include "libssh/ssh2.h" + +#include "libssh/libssh_acton.h" + +/* Copies of #defines and static helpers from src/channels.c @ 0.11.0. */ +#define CHANNEL_MAX_PACKET 32768 +#define WINDOW_DEFAULT (64 * CHANNEL_MAX_PACKET) + +static int grow_window(ssh_session session, ssh_channel channel) +{ + uint32_t used; + uint32_t increment; + int rc; + + /* Calculate the increment taking into account what the peer may still + * send (local_window) and what we've already buffered (stdout_buffer and + * stderr_buffer). + */ + used = channel->local_window; + if (channel->stdout_buffer != NULL) { + used += ssh_buffer_get_len(channel->stdout_buffer); + } + if (channel->stderr_buffer != NULL) { + used += ssh_buffer_get_len(channel->stderr_buffer); + } + /* Avoid a negative increment in case the peer sent more than the window + * allowed */ + increment = WINDOW_DEFAULT > used ? WINDOW_DEFAULT - used : 0; + /* Don't grow until we can request at least half a window */ + if (increment < (WINDOW_DEFAULT / 2)) { + SSH_LOG(SSH_LOG_DEBUG, + "growing window (channel %" PRIu32 ":%" PRIu32 ") to %" PRIu32 + " bytes : not needed (%" PRIu32 " bytes)", + channel->local_channel, channel->remote_channel, + WINDOW_DEFAULT, channel->local_window); + + return SSH_OK; + } + + rc = ssh_buffer_pack(session->out_buffer, + "bdd", + SSH2_MSG_CHANNEL_WINDOW_ADJUST, + channel->remote_channel, + increment); + if (rc != SSH_OK) { + ssh_set_error_oom(session); + goto error; + } + + if (ssh_packet_send(session) == SSH_ERROR) { + goto error; + } + + SSH_LOG(SSH_LOG_DEBUG, + "growing window (channel %" PRIu32 ":%" PRIu32 ") by %" PRIu32 + " bytes", + channel->local_channel, + channel->remote_channel, + increment); + + channel->local_window += increment; + + return SSH_OK; +error: + ssh_buffer_reinit(session->out_buffer); + + return SSH_ERROR; +} + +static bool ssh_channel_has_unread_data(ssh_channel channel) +{ + if (channel == NULL) { + return false; + } + + if ((channel->stdout_buffer && + ssh_buffer_get_len(channel->stdout_buffer) > 0) || + (channel->stderr_buffer && + ssh_buffer_get_len(channel->stderr_buffer) > 0)) + { + return true; + } + + return false; +} + +int ssh_session_handle_poll(ssh_session session, int revents) +{ + ssh_poll_handle ph; + int rc; + + if (session == NULL || session->socket == NULL) { + return SSH_ERROR; + } + + ph = ssh_socket_get_poll_handle(session->socket); + if (ph == NULL) { + return SSH_ERROR; + } + + rc = ssh_socket_pollcallback(ph, + ssh_socket_get_fd(session->socket), + revents, + session->socket); + if (rc < 0) { + return SSH_ERROR; + } + + return SSH_OK; +} + +/** + * @brief Reads buffered data from a channel without polling the socket. + * + * @param[in] channel The channel to read from. + * + * @param[out] dest The destination buffer which will get the data. + * + * @param[in] count The count of bytes to be read. + * + * @param[in] is_stderr A boolean value to mark reading from the stderr flow. + * + * @return The number of bytes read, SSH_AGAIN if nothing is + * available, SSH_ERROR on error, and SSH_EOF if the + * channel is EOF. + */ +int ssh_channel_read_buffered(ssh_channel channel, + void *dest, + uint32_t count, + int is_stderr) +{ + ssh_session session; + ssh_buffer stdbuf; + uint32_t len; + + if (channel == NULL) { + return SSH_ERROR; + } + if (dest == NULL) { + ssh_set_error_invalid(channel->session); + return SSH_ERROR; + } + + session = channel->session; + if (count == 0) { + return 0; + } + + stdbuf = channel->stdout_buffer; + if (is_stderr) { + stdbuf = channel->stderr_buffer; + } + + if (session->session_state == SSH_SESSION_STATE_ERROR) { + return SSH_ERROR; + } + + if (channel->state == SSH_CHANNEL_STATE_CLOSED) { + ssh_set_error(session, + SSH_FATAL, + "Remote channel is closed."); + return SSH_ERROR; + } + + len = ssh_buffer_get_len(stdbuf); + if (len == 0) { + if (channel->remote_eof) { + return SSH_EOF; + } + return SSH_AGAIN; + } + + if (len > count) { + len = count; + } + + memcpy(dest, ssh_buffer_get(stdbuf), len); + ssh_buffer_pass_bytes(stdbuf, len); + if (channel->counter != NULL) { + channel->counter->in_bytes += len; + } + + /* Try completing the delayed_close */ + if (channel->delayed_close && !ssh_channel_has_unread_data(channel)) { + channel->state = SSH_CHANNEL_STATE_CLOSED; + } + + if (grow_window(session, channel) == SSH_ERROR) { + return SSH_ERROR; + } + + return len; +} diff --git a/src/lib.ext.c b/src/lib.ext.c index 926b683..09781bd 100644 --- a/src/lib.ext.c +++ b/src/lib.ext.c @@ -90,6 +90,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test_ssh.act b/src/test_ssh.act index 3538ff3..662fa64 100644 --- a/src/test_ssh.act +++ b/src/test_ssh.act @@ -13,7 +13,7 @@ import testing def _test_version(): - testing.assertEqual("0.11.0/mbedtls", ssh.version()) + testing.assertEqual("0.12.0/mbedtls", ssh.version()) TEST_USER = "testuser"