bytedeco/javacpp

javacpp-generated `pop_back()` returns a `@ByRef` alias to the element it just destroyed

Open

#830 opened on Jul 30, 2026

 (1 comment) (0 reactions) (0 assignees)Java (620 forks)batch import
bughelp wanted

Repository metrics

Stars
 (4,279 stars)
PR merge metrics
 (No merged PRs in 30d)

Description

The pop_back() for vector and deque as generated by javacpp always returns a dangling reference for anything other than a primitive type.

public Widget pop_back() {
    long size = size();
    Widget value = get(size - 1);   // @ByRef alias to &vec[size-1]
    resize(size - 1);               // destroys vec[size-1]  -> ~Widget() runs
    return value;                   // dangling: element already destroyed
}

For a T with a non-trivial destructor (e.g. PIMPL freeing a unique_ptr), the returned peer is dangling the instant pop_back() returns. Any use is a use-after-free. (The accessors get(i)/front()/back() are fine: their alias is valid at return and only dangles on later mutation, per #672. pop_back() is the only method that mutates its own referent before returning it.)

Reproduce (see attached jcpp-popback-repro/)

Widget owns a heap pointer; its destructor frees and nulls it. Any call on a widget after that is a use-after-free. In the reproduction, ~Widget() also sets the handle it holds to null so that any additional calls result in a deterministic SIGSEGV.

VecWidget v = new VecWidget();
v.push_back(new Widget(10)); v.push_back(new Widget(20)); v.push_back(new Widget(30));

v.back().value();            // 30 — alias into a live element is fine
v.pop_back().value();        // SIGSEGV in Java_..._Widget_value: reads a destroyed Widget
# SIGSEGV (0xb) ...
# C  [libjniwidget.dylib+...]  Java_com_example_demo_Widget_value+...

Reproduced on macOS arm64 / Zulu OpenJDK 21 / Apple clang 17, JavaCPP 1.5.13.

Suggested fix: pop-and-move-out

Bind a native pop_back to a small free function that moves the element out of the vector by value, and let the existing @ByVal-return path give it an owned peer:

// generated Java
public T pop_back() { return popBack(this); }
@Namespace("…") @Name("JavaCPP_popBack") private static native @ByVal T popBack(@ByRef VecT v);
// emitted once alongside the container (T = element type)
static T JavaCPP_popBack(std::vector<T>& v) { T value = std::move(v.back()); v.pop_back(); return value; }

The result is @ByVal so that it never aliases vector storage; pop_back() then destroys only the moved-from husk.

I verified the move-out approach end to end against the attached repro: the same read that SIGSEGVs through the generated pop_back() returns the correct owned value with no crash (see MainFixed.java / demo::popBackOut).

Let me know if you would like me to create a patch.

dangling-popback-reproduction.zip

Contributor guide