Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/main/docs/project-requirements.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ store in `PosixAPIHolder.POSIX_API`.

| POSIX-FN-003
| Deterministic error handling
| Translate every native failure into a `PosixRuntimeException`;
expose `lastError()` and `strerror(int)`.
| Translate unexpected native failures into a `PosixRuntimeException`;
operations with a documented boolean failure result return `false` for
their expected errors; expose `lastError()` and `strerror(int)`.

| POSIX-FN-004
| File-descriptor lifecycle
Expand All @@ -44,7 +45,10 @@ expose `lastError()` and `strerror(int)`.
| Support `mmap`, `munmap`, `madvise`, `msync`, `mlock`, `mlock2`,
`mlockall`; degrade gracefully on unsupported kernels/VMs; provide
full flag coverage via `MMapProt`, `MMapFlag`, `MAdviseFlag`,
`MSyncFlag`, `MclFlag`.
`MSyncFlag`, `MclFlag`. Memory locking returns `false` for resource,
permission, transient and unsupported-operation errors; other errors
raise `PosixRuntimeException` with the saved `errno`. Raw `mlock2`
syscalls are used only for recognised Linux CPU ABIs.

| POSIX-FN-006
| File space pre-allocation
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/openhft/posix/PosixAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ default boolean mlock(long addr, long length) {
* first access when {@code lockOnFault} is {@code true}. The default
* implementation returns {@code false}. Failure reasons mirror those of
* {@code mlock} and also include lack of kernel support for {@code mlock2}.
* The JNR provider returns {@code false} for Linux {@code ENOSYS}; on macOS,
* where {@code mlock2} is absent, it performs an eager {@code mlock} instead.
*
* @param addr start address
* @param length number of bytes to lock
Expand Down
123 changes: 86 additions & 37 deletions src/main/java/net/openhft/posix/internal/jnr/JNRPosixAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,23 @@
import jnr.ffi.provider.FFIProvider;
import net.openhft.posix.*;
import net.openhft.posix.internal.UnsafeMemory;
import net.openhft.posix.internal.core.Jvm;
import net.openhft.posix.internal.core.OS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Locale;
import java.util.Objects;
import java.util.function.IntSupplier;

import static net.openhft.posix.internal.UnsafeMemory.UNSAFE;

/**
* Implementation of {@link PosixAPI} using JNR (Java Native Runtime).
*
* <p>Where a JNR binding is absent this class issues raw syscalls using
* hard-coded numbers chosen for common architectures. If the kernel does not
* recognise a number the call gracefully falls back to the available wrapper.</p>
* <p>Where the {@code mlock2} JNR binding is absent this class issues a raw
* syscall only for a recognised Linux CPU ABI. Unsupported kernels, operating
* systems and ABIs return {@code false} rather than invoking an unrelated syscall.</p>
*/
public final class JNRPosixAPI implements PosixAPI {

Expand All @@ -40,25 +41,32 @@ public final class JNRPosixAPI implements PosixAPI {
static final int LOCK_EX = 2;
static final int LOCK_UN = 8;
static final int MLOCK_ONFAULT = 1;
static final int SYS_mlock2; // mlock2 syscall value

static {
// These cover the main cases. Full list under https://github.com/torvalds/linux/tree/master/arch
SYS_mlock2 = Jvm.isArm() ? 390
: Jvm.is64bit() ? 325 : 376;
}
static final int MLOCK2_UNAVAILABLE = Integer.MIN_VALUE;
static final int SYS_MLOCK2 = mlock2SyscallNumber(
NATIVE_PLATFORM.getOS(), System.getProperty("os.arch", ""));

// JNR interface for POSIX functions
private final JNRPosixInterface jnr;

private final IntSupplier lastErrorSupplier;

private final int mlock2SyscallNumber;

// Supplier for gettid method
private final IntSupplier gettid;

/**
* Constructs a JNRPosixAPI instance and initializes the JNR interface and gettid supplier.
*/
public JNRPosixAPI() {
jnr = LibraryUtil.load(JNRPosixInterface.class, STANDARD_C_LIBRARY_NAME);
this(LibraryUtil.load(JNRPosixInterface.class, STANDARD_C_LIBRARY_NAME),
RUNTIME::getLastError, SYS_MLOCK2);
}

JNRPosixAPI(JNRPosixInterface jnr, IntSupplier lastErrorSupplier, int mlock2SyscallNumber) {
this.jnr = Objects.requireNonNull(jnr);
this.lastErrorSupplier = Objects.requireNonNull(lastErrorSupplier);
this.mlock2SyscallNumber = mlock2SyscallNumber;
gettid = getGettid();
}

Expand Down Expand Up @@ -117,8 +125,8 @@ public int close(int fd) {
* @param msg The message to include in the exception.
* @return A {@link PosixRuntimeException} with the specified message and last error.
*/
private static RuntimeException throwPosixException(String msg) {
final int lastError = RUNTIME.getLastError();
private RuntimeException throwPosixException(String msg) {
final int lastError = lastErrorSupplier.getAsInt();
for (Errno errno : Errno.values()) {
if (errno.intValue() == lastError)
throw new PosixRuntimeException(msg + "error " + errno, lastError);
Expand All @@ -132,7 +140,7 @@ public long mmap(long addr, long length, int prot, int flags, int fd, long offse
final Pointer wrap = addr == 0 ? NULL : Pointer.wrap(RUNTIME, addr);
final long mmap = jnr.mmap(wrap, length, prot, flags, fd, offset);
if (mmap == 0 || mmap == -1) {
final int lastError = RUNTIME.getLastError();
final int lastError = lastErrorSupplier.getAsInt();
for (Errno errno : Errno.values()) {
if (errno.intValue() == lastError)
throw new PosixRuntimeException(errno.toString(), lastError);
Expand All @@ -155,37 +163,78 @@ private int mlock2Native(long addr, long length, boolean lockOnFault) {
if (!lockOnFault || OS.isMacOSX())
return jnr.mlock(addr, length);

// Older glibc versions do not include a wrapper for mlock2, so use syscall for generality
return jnr.syscall(SYS_mlock2, addr, length, MLOCK_ONFAULT);
try {
return jnr.mlock2(addr, length, MLOCK_ONFAULT);
} catch (UnsatisfiedLinkError unavailable) {
if (mlock2SyscallNumber < 0)
return MLOCK2_UNAVAILABLE;
return jnr.syscall(mlock2SyscallNumber, addr, length, MLOCK_ONFAULT);
}
}

@Override
public boolean mlock(long addr, long length) {
if(Jvm.isAzul()) {
LOGGER.warn("mlock called but ignored for Azul");
return true; // no-op on Azul, ignore
}

int err = jnr.mlock(addr, length);
if (err == 0)
return true;
if (err == Errno.ENOMEM.intValue())
return false;
throw throwPosixException("mlock length: " + length + " ");
return handleMlockResult(jnr.mlock(addr, length), "mlock length: " + length + " ");
}

@Override
public boolean mlock2(long addr, long length, boolean lockOnFault) {
if(Jvm.isAzul()) {
LOGGER.warn("mlock2 called but ignored for Azul");
return true; // no-op on Azul, ignore
}
int err = mlock2Native(addr, length, lockOnFault);
if (err == 0)
return handleMlockResult(mlock2Native(addr, length, lockOnFault), "mlock2 length: " + length + " ");
}

/**
* Interprets an {@code mlock}/{@code mlock2} native result honestly.
* <p>
* POSIX {@code mlock} returns {@code 0} on success and {@code -1} on failure with the reason in
* {@code errno} - so the previous {@code result == ENOMEM} comparison could never match (the
* result is {@code -1}, not the errno value) and every failure was thrown. It also short-circuited
* to {@code return true} on Azul without locking anything, reporting success while the memory was
* not locked. Both are corrected here: call the native op on every vendor, read {@code errno} on
* failure, and map the expected "cannot lock" conditions (permission / limit / transient) to
* {@code false} while still throwing on genuinely unexpected errors. This is what unblocked the
* original Azul/Zing {@code ENOMEM} case (Posix#15) - gracefully, without a false success.
*/
private boolean handleMlockResult(int result, String msg) {
if (result == 0)
return true;
if (err == Errno.ENOMEM.intValue())
if (result == MLOCK2_UNAVAILABLE) {
LOGGER.warn(msg + "not locked: mlock2 unavailable for architecture " +
System.getProperty("os.arch", "unknown"));
return false;
throw throwPosixException("mlock2 length: " + length + " ");
}
final int lastError = lastErrorSupplier.getAsInt();
if (lastError == Errno.ENOMEM.intValue()
|| lastError == Errno.EPERM.intValue()
|| lastError == Errno.EAGAIN.intValue()
|| lastError == Errno.ENOSYS.intValue()) {
LOGGER.warn(msg + "not locked: " + errnoName(lastError));
return false;
}
throw throwPosixException(msg);
}

private static String errnoName(int lastError) {
for (Errno errno : Errno.values())
if (errno.intValue() == lastError)
return errno.toString();
return "errno " + lastError;
}

static int mlock2SyscallNumber(Platform.OS operatingSystem, String architecture) {
if (operatingSystem != Platform.OS.LINUX)
return -1;
final String arch = architecture.toLowerCase(Locale.ROOT);
if (arch.equals("x86_64") || arch.equals("amd64"))
return 325;
if (arch.equals("x86") || arch.matches("i[3-6]86"))
return 376;
if (arch.equals("aarch64") || arch.equals("arm64"))
return 284;
if (arch.equals("arm") || arch.equals("arm32") || arch.matches("armv[5-8].*"))
return 390;
if (arch.equals("riscv64"))
return 284;
return -1;
}

@Override
Expand Down Expand Up @@ -218,7 +267,7 @@ private void tryMLockAll(int flags) {
continue;
final long kb = mapping.length() / 1024;
if (ret != 0) {
final int lastError = RUNTIME.getLastError();
final int lastError = lastErrorSupplier.getAsInt();
for (Errno errno : Errno.values()) {
if (errno.intValue() == lastError)
System.out.println(mapping + "len: " + kb + " KiB " + " " + errno);
Expand Down
10 changes: 8 additions & 2 deletions src/test/java/net/openhft/posix/PosixAPIHolderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ public void loadPosixApiInitialisesProviderOnce() {

@Test
public void useNoOpPosixApiSwitchesToNoOp() {
PosixAPIHolder.useNoOpPosixApi();
assertTrue(PosixAPIHolder.POSIX_API instanceof NoOpPosixAPI);
PosixAPIHolder.loadPosixApi();
final PosixAPI original = PosixAPIHolder.POSIX_API;
try {
PosixAPIHolder.useNoOpPosixApi();
assertTrue(PosixAPIHolder.POSIX_API instanceof NoOpPosixAPI);
} finally {
PosixAPIHolder.POSIX_API = original;
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
*/
package net.openhft.posix.internal.jnr;

import net.openhft.posix.MMapFlag;
import net.openhft.posix.MMapProt;
import net.openhft.posix.OpenFlag;
import net.openhft.posix.PosixAPI;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assume.assumeTrue;

/**
* Regression test for Posix#15: {@code mlock} must not report success without actually locking memory.
* <p>
* The previous implementation returned {@code true} on Azul JVMs without calling {@code mlock} at all,
* and (on every vendor) compared the native return value directly with {@code ENOMEM} instead of reading
* {@code errno} after a {@code -1}. This test proves the reported outcome matches reality by reading the
* process's locked-memory count ({@code VmLck} in {@code /proc/self/status}) before and after the call.
*/
public class MlockLocksMemoryTest {

private static final long LOCK_LEN = 4096;

@Test
public void mlockReportedSuccessImpliesMemoryActuallyLocked() throws IOException {
assumeTrue("enable with -Dposix.mlock.integration=true",
Boolean.getBoolean("posix.mlock.integration"));
assumeTrue("needs Linux /proc/self/status", new File("/proc/self/status").exists());
final PosixAPI posix = PosixAPI.posix();
assumeTrue("needs the JNR provider", posix instanceof JNRPosixAPI);
final JNRPosixAPI jnr = (JNRPosixAPI) posix;

final Path file = Files.createTempFile("mlock", ".test");
final String filename = file.toAbsolutePath().toString();
final int fd = jnr.open(filename, OpenFlag.O_RDWR, 0666);
try {
assertEquals(0, jnr.ftruncate(fd, 1L << 16));
final long addr = jnr.mmap(0, 1L << 16, MMapProt.PROT_READ_WRITE, MMapFlag.SHARED, fd, 0L);
assertNotEquals(-1, addr);
try {
final long lockedBefore = vmLckKb();
final boolean locked = jnr.mlock(addr, LOCK_LEN);
final long lockedAfter = vmLckKb();

if (locked) {
// The honest contract: a true result means the pages are genuinely locked.
assertNotEquals("mlock returned true but VmLck did not increase (false success)",
lockedBefore, lockedAfter);
} else {
// A false result (e.g. RLIMIT_MEMLOCK exhausted) must not have locked anything.
assertEquals("mlock returned false but VmLck increased", lockedBefore, lockedAfter);
assumeTrue("environment cannot lock memory (RLIMIT_MEMLOCK); nothing to prove", false);
}
} finally {
jnr.munmap(addr, 1L << 16);
}
} finally {
jnr.close(fd);
Files.deleteIfExists(file);
}
}

/** Locked memory in KiB from {@code /proc/self/status} ({@code VmLck}), or 0 if absent. */
private static long vmLckKb() throws IOException {
for (String line : Files.readAllLines(new File("/proc/self/status").toPath()))
if (line.startsWith("VmLck:"))
return Long.parseLong(line.replaceAll("[^0-9]", ""));
return 0;
}
}
Loading