!> @file cortex_link.f90 !> @brief Fortran wrapper around the socket shim (cortex_link.c) !! plus URI parsing and length-prefix frame I/O. !! !! Phase G: §11.4 of the master spec. The C shim provides a BSD-sockets !! (POSIX) and a Winsock2 (Windows) backend, so attached mode runs on !! Linux, macOS, and Windows alike. module cortex_link use, intrinsic :: iso_c_binding, only: c_int, c_size_t, c_char, c_null_char, c_ptr, c_loc use, intrinsic :: iso_fortran_env, only: int8, int64 use mpi_runtime, only: parallel_fatal implicit none private public :: parse_uri public :: cortex_open public :: cortex_send_frame public :: cortex_recv_frame public :: cortex_close_fd public :: be_u32_to_i64 public :: is_frame_len_valid public :: is_send_frame_valid public :: MAX_FRAME_BYTES public :: MAX_SEND_FRAME_BYTES !> Upper bound on a single length-prefixed INBOUND frame field (header !! or payload), enforced before allocation in =cortex_recv_frame=. The !! Cortex peer is on the loopback TCP port, but it is still untrusted !! input: without a cap a buggy/compromised peer can request ~4 GiB !! per field and exhaust memory. 64 MiB is generously above any !! legitimate request frame (headers are small JSON; bulk binary data !! rides the COPY_SOLUTION *reply*, which the worker sizes itself, not !! an inbound request). This cap deliberately does NOT apply to the !! send path — see =MAX_SEND_FRAME_BYTES=. integer(int64), parameter :: MAX_FRAME_BYTES = 64_int64 * 1024_int64 * 1024_int64 !> Upper bound on an OUTBOUND frame field: the u32 wire length prefix !! (spec §12.2) can express at most 2^32-1 bytes. Legitimate !! COPY_SOLUTION replies routinely exceed the 64 MiB inbound cap (any !! 2D grid past ~1182² with 6 primitives), so the send side must not !! reuse =MAX_FRAME_BYTES= (audit 2026-07-06 N1). Handlers that build !! bulk replies pre-check against this limit and return a reply-level !! error instead of ever tripping the fatal send-side guard. integer(int64), parameter :: MAX_SEND_FRAME_BYTES = 4294967295_int64 interface function c_cortex_connect_tcp(host, port) bind(C, name="cortex_connect_tcp") result(fd) import :: c_int, c_char character(kind=c_char), intent(in) :: host(*) integer(c_int), value :: port integer(c_int) :: fd end function c_cortex_connect_tcp function c_cortex_send_all(fd, buf, n) bind(C, name="cortex_send_all") result(r) import :: c_int, c_size_t, c_ptr integer(c_int), value :: fd type(c_ptr), value :: buf integer(c_size_t), value :: n integer(c_size_t) :: r end function c_cortex_send_all function c_cortex_recv_all(fd, buf, n) bind(C, name="cortex_recv_all") result(r) import :: c_int, c_size_t, c_ptr integer(c_int), value :: fd type(c_ptr), value :: buf integer(c_size_t), value :: n integer(c_size_t) :: r end function c_cortex_recv_all function c_cortex_close(fd) bind(C, name="cortex_close") result(r) import :: c_int integer(c_int), value :: fd integer(c_int) :: r end function c_cortex_close end interface contains !> Parse a "tcp://host:port" URI into host string and integer port. !! Errors fatally on a malformed URI — bug in the caller. subroutine parse_uri(uri, host, port) character(len=*), intent(in) :: uri character(len=*), intent(out) :: host integer, intent(out) :: port integer :: colon_pos, last_colon, ios ! Guard the fixed-length scheme compare: a CLI value shorter than ! "tcp://x" (7 chars) cannot be a valid URI, and uri(1:6) on a ! shorter string is an out-of-bounds substring. if (len_trim(uri) < 7) then error stop 'cortex_link: URI too short (expected tcp://host:port)' end if if (uri(1:6) /= "tcp://") then error stop 'cortex_link: URI must start with "tcp://"' end if ! Find the LAST colon (host could be an IPv4 literal which doesn't ! contain colons, but be robust to future IPv6). last_colon = 0 do colon_pos = 7, len_trim(uri) if (uri(colon_pos:colon_pos) == ":") last_colon = colon_pos end do if (last_colon == 0) then error stop 'cortex_link: URI missing port (expected tcp://host:port)' end if ! Reject a host longer than the caller's buffer rather than ! silently truncating it (a truncated host would connect to the ! wrong/no endpoint). if (last_colon - 1 - 7 + 1 > len(host)) then error stop 'cortex_link: URI host exceeds host buffer length' end if host = uri(7:last_colon - 1) ! Validate the port parse instead of crashing with an unformatted ! runtime error on a non-numeric/empty port. read (uri(last_colon + 1:), *, iostat=ios) port if (ios /= 0) then error stop 'cortex_link: URI port is not a valid integer' end if end subroutine parse_uri !> Open a TCP loopback connection to the Cortex server. subroutine cortex_open(uri, fd) character(len=*), intent(in) :: uri integer, intent(out) :: fd character(len=128) :: host character(kind=c_char, len=130) :: host_c integer :: port call parse_uri(uri, host, port) host_c = trim(host)//c_null_char fd = int(c_cortex_connect_tcp(host_c, int(port, c_int))) if (fd < 0) then error stop 'cortex_link: cortex_connect_tcp failed (check Cortex is listening)' end if end subroutine cortex_open !> Send a length-prefixed frame: 4-byte BE header length, header !! bytes, 4-byte BE payload length, payload bytes. Both length !! prefixes are always present (payload length is zero when payload !! is empty; spec §12.2). subroutine cortex_send_frame(fd, header, payload) integer, intent(in) :: fd integer(int8), intent(in), target :: header(:) integer(int8), intent(in), target :: payload(:) integer(int8), target :: header_len_be(4), payload_len_be(4) ! The only send-side wall is the u32 length prefix itself: a field ! longer than 2^32-1 bytes cannot be framed at all. Callers building ! bulk replies (COPY_SOLUTION) pre-check their sizes and reply with a ! protocol-level error instead, so tripping this guard is a caller ! bug, not a data-dependent condition. Do NOT gate sends on the 64 MiB ! MAX_FRAME_BYTES request cap — replies legitimately exceed it ! (audit 2026-07-06 N1). Once validated, the int64 value is in ! [0, 2^32-1] and pack_be_u32 packs it directly from int64 (an int32 ! round-trip would be an out-of-range conversion past 2^31-1). if (.not. is_send_frame_valid(size(header, kind=int64)) .or. & .not. is_send_frame_valid(size(payload, kind=int64))) & call parallel_fatal('cortex_link: outgoing frame exceeds the u32 wire limit (caller must pre-check)') call pack_be_u32(size(header, kind=int64), header_len_be) call pack_be_u32(size(payload, kind=int64), payload_len_be) call send_bytes(fd, header_len_be) if (size(header) > 0) call send_bytes(fd, header) call send_bytes(fd, payload_len_be) if (size(payload) > 0) call send_bytes(fd, payload) end subroutine cortex_send_frame !> Receive a length-prefixed frame. Both header and payload are !! returned as allocatable byte arrays (caller owns). subroutine cortex_recv_frame(fd, header, payload) integer, intent(in) :: fd integer(int8), allocatable, intent(out) :: header(:) integer(int8), allocatable, intent(out) :: payload(:) integer(int8) :: len_be(4) integer(int64) :: header_len, payload_len integer :: alloc_stat call recv_bytes(fd, len_be) header_len = be_u32_to_i64(len_be) if (.not. is_frame_len_valid(header_len)) & error stop 'cortex_link: header frame length out of range (negative or > MAX_FRAME_BYTES)' allocate (header(int(header_len)), stat=alloc_stat) if (alloc_stat /= 0) error stop 'cortex_link: cortex_recv_frame: header allocation failed' if (header_len > 0) call recv_bytes(fd, header) call recv_bytes(fd, len_be) payload_len = be_u32_to_i64(len_be) if (.not. is_frame_len_valid(payload_len)) & error stop 'cortex_link: payload frame length out of range (negative or > MAX_FRAME_BYTES)' allocate (payload(int(payload_len)), stat=alloc_stat) if (alloc_stat /= 0) error stop 'cortex_link: cortex_recv_frame: payload allocation failed' if (payload_len > 0) call recv_bytes(fd, payload) end subroutine cortex_recv_frame subroutine cortex_close_fd(fd) integer, intent(in) :: fd integer(c_int) :: rc rc = c_cortex_close(int(fd, c_int)) end subroutine cortex_close_fd ! ---- internals ---- subroutine send_bytes(fd, buf) integer, intent(in) :: fd integer(int8), intent(in), target :: buf(:) integer(c_size_t) :: sent ! Use size(..., kind=int64) so the element count is correct past 2^31-1. ! After the MAX_FRAME_BYTES cap in cortex_send_frame the cast to c_size_t ! is safe on any LP64 / LLP64 platform (c_size_t >= 64 bits in practice). sent = c_cortex_send_all(int(fd, c_int), c_loc(buf), & int(size(buf, kind=int64), c_size_t)) if (sent /= int(size(buf, kind=int64), c_size_t)) then error stop 'cortex_link: send_bytes: short send (peer closed?)' end if end subroutine send_bytes subroutine recv_bytes(fd, buf) integer, intent(in) :: fd integer(int8), intent(inout), target :: buf(:) integer(c_size_t) :: got got = c_cortex_recv_all(int(fd, c_int), c_loc(buf), int(size(buf), c_size_t)) if (got /= size(buf)) then error stop 'cortex_link: recv_bytes: short recv (peer closed?)' end if end subroutine recv_bytes !> Pack an UNSIGNED 32-bit length prefix from an int64 value in !! [0, 4294967295]. Takes int64 so a length in (2^31-1, 2^32-1] — !! legitimate for large COPY_SOLUTION replies — never round-trips !! through int32 (an out-of-range conversion, processor-dependent). !! Inverse of =be_u32_to_i64=. pure subroutine pack_be_u32(n, out) integer(int64), intent(in) :: n integer(int8), intent(out) :: out(4) out(1) = int(iand(ishft(n, -24), int(z'FF', int64)), int8) out(2) = int(iand(ishft(n, -16), int(z'FF', int64)), int8) out(3) = int(iand(ishft(n, -8), int(z'FF', int64)), int8) out(4) = int(iand(n, int(z'FF', int64)), int8) end subroutine pack_be_u32 !> Decode a 4-byte big-endian length prefix as an UNSIGNED 32-bit !! value widened to int64. Each byte is masked into int64 before the !! shift/OR, so a wire value with the high bit set (0x80000000.. !! 0xFFFFFFFF) decodes as a large positive int64 — not a negative !! int32 (the desync bug). Range: [0, 4294967295]. pure function be_u32_to_i64(buf) result(n) integer(int8), intent(in) :: buf(4) integer(int64) :: n n = ior(ishft(iand(int(buf(1), int64), int(z'FF', int64)), 24), & ishft(iand(int(buf(2), int64), int(z'FF', int64)), 16)) n = ior(n, ior(ishft(iand(int(buf(3), int64), int(z'FF', int64)), 8), & iand(int(buf(4), int64), int(z'FF', int64)))) end function be_u32_to_i64 !> A decoded frame length is acceptable iff it is non-negative and !! within the =MAX_FRAME_BYTES= cap. (Non-negativity is structurally !! guaranteed by =be_u32_to_i64=, but kept explicit so the predicate !! is correct for any int64 caller.) pure function is_frame_len_valid(n) result(ok) integer(int64), intent(in) :: n logical :: ok ok = (n >= 0_int64) .and. (n <= MAX_FRAME_BYTES) end function is_frame_len_valid !> Check that a send-side frame field (header or payload) can be !! represented by the u32 wire length prefix. Deliberately NOT !! symmetric with =is_frame_len_valid=: the 64 MiB inbound cap guards !! against an untrusted peer's *request* sizes, while outbound frames !! are sized by the worker itself and legitimately carry bulk !! COPY_SOLUTION replies far beyond 64 MiB (audit 2026-07-06 N1). pure function is_send_frame_valid(n) result(ok) integer(int64), intent(in) :: n logical :: ok ok = (n >= 0_int64) .and. (n <= MAX_SEND_FRAME_BYTES) end function is_send_frame_valid end module cortex_link