diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java index fc438320078..f84f27cb4a9 100644 --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -232,6 +232,7 @@ module java.base { exports jdk.internal.ref to java.desktop, java.net.http, + java.smartcardio, jdk.naming.dns; exports jdk.internal.reflect to java.logging, diff --git a/src/java.smartcardio/share/classes/javax/smartcardio/package-info.java b/src/java.smartcardio/share/classes/javax/smartcardio/package-info.java index ef6489509df..9fa4bb18b66 100644 --- a/src/java.smartcardio/share/classes/javax/smartcardio/package-info.java +++ b/src/java.smartcardio/share/classes/javax/smartcardio/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -81,12 +81,15 @@ * CardTerminal terminal = terminals.get(0); * // establish a connection with the card * Card card = terminal.connect("T=0"); - * System.out.println("card: " + card); - * CardChannel channel = card.getBasicChannel(); - * ResponseAPDU r = channel.transmit(new CommandAPDU(c1)); - * System.out.println("response: " + toString(r.getBytes())); - * // disconnect - * card.disconnect(false); + * try { + * System.out.println("card: " + card); + * CardChannel channel = card.getBasicChannel(); + * ResponseAPDU r = channel.transmit(new CommandAPDU(c1)); + * System.out.println("response: " + toString(r.getBytes())); + * } finally { + * // disconnect + * card.disconnect(false); + * } * * * @since 1.6 diff --git a/src/java.smartcardio/share/classes/sun/security/smartcardio/CardImpl.java b/src/java.smartcardio/share/classes/sun/security/smartcardio/CardImpl.java index f268f50d0ad..100ba76f7dd 100644 --- a/src/java.smartcardio/share/classes/sun/security/smartcardio/CardImpl.java +++ b/src/java.smartcardio/share/classes/sun/security/smartcardio/CardImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,9 @@ package sun.security.smartcardio; import jdk.internal.util.OperatingSystem; +import jdk.internal.ref.CleanerFactory; +import java.lang.ref.Cleaner; +import java.lang.ref.Reference; import javax.smartcardio.*; import static sun.security.smartcardio.PCSC.*; @@ -43,9 +46,6 @@ final class CardImpl extends Card { // the terminal that created this card private final TerminalImpl terminal; - // the native SCARDHANDLE - final long cardId; - // atr of this card private final ATR atr; @@ -55,12 +55,45 @@ final class CardImpl extends Card { // the basic logical channel (channel 0) private final ChannelImpl basicChannel; - // state of this card connection - private volatile State state; - // thread holding exclusive access to the card, or null private volatile Thread exclusiveThread; + /* State and code for cleanup */ + static final class Context implements Runnable { + // the native SCARDHANDLE + final long cardId; + + // state of this card connection + private volatile State state; + + private Context(long cardId, State state) { + this.cardId = cardId; + this.state = state; + } + + /** + * The cleaning action calls SCardDisconnect if the card state is OK, + * otherwise it does nothing. + */ + public void run() { + if (state == State.OK) { + state = State.DISCONNECTED; + try { + SCardDisconnect(cardId, SCARD_LEAVE_CARD); + } catch (PCSCException e) { + // This will be swallowed if thrown when run by the Cleaner + // thread, and never thrown if called via Cleanable.clean() + // (only called if state != OK.) + throw new RuntimeException(e); + } + } + } + } + + final Context context; + private final Cleaner.Cleanable cleanable; + + CardImpl(TerminalImpl terminal, String protocol) throws PCSCException { this.terminal = terminal; int sharingMode = SCARD_SHARE_SHARED; @@ -83,36 +116,50 @@ final class CardImpl extends Card { } else { throw new IllegalArgumentException("Unsupported protocol " + protocol); } - cardId = SCardConnect(terminal.contextId, terminal.name, + long localCardId = SCardConnect(terminal.contextId, terminal.name, sharingMode, connectProtocol); + + this.context = new Context(localCardId, State.OK); + this.cleanable = CleanerFactory.cleaner().register(this, this.context); + byte[] status = new byte[2]; - byte[] atrBytes = SCardStatus(cardId, status); + byte[] atrBytes = SCardStatus(localCardId, status); atr = new ATR(atrBytes); this.protocol = status[1] & 0xff; basicChannel = new ChannelImpl(this, 0); - state = State.OK; } void checkState() { - State s = state; - if (s == State.DISCONNECTED) { - throw new IllegalStateException("Card has been disconnected"); - } else if (s == State.REMOVED) { - throw new IllegalStateException("Card has been removed"); + try { + State s = context.state; + if (s == State.DISCONNECTED) { + throw new IllegalStateException("Card has been disconnected"); + } else if (s == State.REMOVED) { + throw new IllegalStateException("Card has been removed"); + } + } finally { + Reference.reachabilityFence(this); } } boolean isValid() { - if (state != State.OK) { - return false; - } - // ping card via SCardStatus try { - SCardStatus(cardId, new byte[2]); - return true; - } catch (PCSCException e) { - state = State.REMOVED; - return false; + if (context.state != State.OK) { + return false; + } + // ping card via SCardStatus + try { + SCardStatus(context.cardId, new byte[2]); + return true; + } catch (PCSCException e) { + context.state = State.REMOVED; + // state has been set != OK. The cleaning action is now a noop. + // Deregister from Cleaner to reduce reference tracking. + cleanable.clean(); + return false; + } + } finally { + Reference.reachabilityFence(this); } } @@ -125,8 +172,15 @@ final class CardImpl extends Card { } void handleError(PCSCException e) { - if (e.code == SCARD_W_REMOVED_CARD) { - state = State.REMOVED; + try { + if (e.code == SCARD_W_REMOVED_CARD) { + context.state = State.REMOVED; + // state has been set != OK. The cleaning action is now a noop. + // Deregister from Cleaner to reduce reference tracking. + cleanable.clean(); + } + } finally { + Reference.reachabilityFence(this); } } @@ -164,21 +218,25 @@ final class CardImpl extends Card { private static byte[] commandOpenChannel = new byte[] {0, 0x70, 0, 0, 1}; public CardChannel openLogicalChannel() throws CardException { - checkSecurity("openLogicalChannel"); - checkState(); - checkExclusive(); try { - byte[] response = SCardTransmit - (cardId, protocol, commandOpenChannel, 0, commandOpenChannel.length); - if ((response.length != 3) || (getSW(response) != 0x9000)) { - throw new CardException - ("openLogicalChannel() failed, card response: " - + PCSC.toString(response)); + checkSecurity("openLogicalChannel"); + checkState(); + checkExclusive(); + try { + byte[] response = SCardTransmit + (context.cardId, protocol, commandOpenChannel, 0, commandOpenChannel.length); + if ((response.length != 3) || (getSW(response) != 0x9000)) { + throw new CardException + ("openLogicalChannel() failed, card response: " + + PCSC.toString(response)); + } + return new ChannelImpl(this, response[0]); + } catch (PCSCException e) { + handleError(e); + throw new CardException("openLogicalChannel() failed", e); } - return new ChannelImpl(this, response[0]); - } catch (PCSCException e) { - handleError(e); - throw new CardException("openLogicalChannel() failed", e); + } finally { + Reference.reachabilityFence(this); } } @@ -193,87 +251,98 @@ final class CardImpl extends Card { } public synchronized void beginExclusive() throws CardException { - checkSecurity("exclusive"); - checkState(); - if (exclusiveThread != null) { - throw new CardException - ("Exclusive access has already been assigned to Thread " - + exclusiveThread.getName()); - } try { - SCardBeginTransaction(cardId); - } catch (PCSCException e) { - handleError(e); - throw new CardException("beginExclusive() failed", e); + checkSecurity("exclusive"); + checkState(); + if (exclusiveThread != null) { + throw new CardException + ("Exclusive access has already been assigned to Thread " + + exclusiveThread.getName()); + } + try { + SCardBeginTransaction(context.cardId); + } catch (PCSCException e) { + handleError(e); + throw new CardException("beginExclusive() failed", e); + } + exclusiveThread = Thread.currentThread(); + } finally { + Reference.reachabilityFence(this); } - exclusiveThread = Thread.currentThread(); } public synchronized void endExclusive() throws CardException { - checkState(); - if (exclusiveThread != Thread.currentThread()) { - throw new IllegalStateException - ("Exclusive access not assigned to current Thread"); - } try { - SCardEndTransaction(cardId, SCARD_LEAVE_CARD); - } catch (PCSCException e) { - handleError(e); - throw new CardException("endExclusive() failed", e); + checkState(); + if (exclusiveThread != Thread.currentThread()) { + throw new IllegalStateException + ("Exclusive access not assigned to current Thread"); + } + try { + SCardEndTransaction(context.cardId, SCARD_LEAVE_CARD); + } catch (PCSCException e) { + handleError(e); + throw new CardException("endExclusive() failed", e); + } finally { + exclusiveThread = null; + } } finally { - exclusiveThread = null; + Reference.reachabilityFence(this); } } public byte[] transmitControlCommand(int controlCode, byte[] command) throws CardException { - checkSecurity("transmitControl"); - checkState(); - checkExclusive(); - if (command == null) { - throw new NullPointerException(); - } try { - byte[] r = SCardControl(cardId, controlCode, command); - return r; - } catch (PCSCException e) { - handleError(e); - throw new CardException("transmitControlCommand() failed", e); + checkSecurity("transmitControl"); + checkState(); + checkExclusive(); + if (command == null) { + throw new NullPointerException(); + } + try { + byte[] r = SCardControl(context.cardId, controlCode, command); + return r; + } catch (PCSCException e) { + handleError(e); + throw new CardException("transmitControlCommand() failed", e); + } + } finally { + Reference.reachabilityFence(this); } } public void disconnect(boolean reset) throws CardException { - if (reset) { - checkSecurity("reset"); - } - if (state != State.OK) { - return; - } - checkExclusive(); try { - SCardDisconnect(cardId, (reset ? SCARD_RESET_CARD : SCARD_LEAVE_CARD)); - } catch (PCSCException e) { - throw new CardException("disconnect() failed", e); + if (reset) { + checkSecurity("reset"); + } + if (context.state != State.OK) { + return; + } + checkExclusive(); + try { + SCardDisconnect(context.cardId, (reset ? SCARD_RESET_CARD : SCARD_LEAVE_CARD)); + } catch (PCSCException e) { + throw new CardException("disconnect() failed", e); + } finally { + context.state = State.DISCONNECTED; + exclusiveThread = null; + // state has been set != OK. The cleaning action is now a noop. + // Deregister from Cleaner to reduce reference tracking. + cleanable.clean(); + } } finally { - state = State.DISCONNECTED; - exclusiveThread = null; + Reference.reachabilityFence(this); } } public String toString() { - return "PC/SC card in " + terminal.name - + ", protocol " + getProtocol() + ", state " + state; - } - - @SuppressWarnings("removal") - protected void finalize() throws Throwable { try { - if (state == State.OK) { - state = State.DISCONNECTED; - SCardDisconnect(cardId, SCARD_LEAVE_CARD); - } + return "PC/SC card in " + terminal.name + + ", protocol " + getProtocol() + ", state " + context.state; } finally { - super.finalize(); + Reference.reachabilityFence(this); } } diff --git a/src/java.smartcardio/share/classes/sun/security/smartcardio/ChannelImpl.java b/src/java.smartcardio/share/classes/sun/security/smartcardio/ChannelImpl.java index 4d89c57e48c..a92ade5e184 100644 --- a/src/java.smartcardio/share/classes/sun/security/smartcardio/ChannelImpl.java +++ b/src/java.smartcardio/share/classes/sun/security/smartcardio/ChannelImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -189,7 +189,7 @@ final class ChannelImpl extends CardChannel { " exceeded maximum " + RESPONSE_ITERATIONS); } byte[] response = SCardTransmit - (card.cardId, card.protocol, command, 0, n); + (card.context.cardId, card.protocol, command, 0, n); int rn = response.length; if (getresponse && (rn >= 2) && (n >= 1)) { // see ISO 7816/2005, 5.1.3 @@ -280,7 +280,7 @@ final class ChannelImpl extends CardChannel { byte[] com = new byte[] {0x00, 0x70, (byte)0x80, 0}; com[3] = (byte)getChannelNumber(); setChannel(com); - byte[] res = SCardTransmit(card.cardId, card.protocol, com, 0, com.length); + byte[] res = SCardTransmit(card.context.cardId, card.protocol, com, 0, com.length); if (isOK(res) == false) { throw new CardException("close() failed: " + PCSC.toString(res)); } diff --git a/test/jdk/TEST.groups b/test/jdk/TEST.groups index 70d74ed75d9..a4513d35de0 100644 --- a/test/jdk/TEST.groups +++ b/test/jdk/TEST.groups @@ -666,6 +666,7 @@ jdk_security_manual_no_input = \ com/sun/crypto/provider/Cipher/AEAD/GCMIncrementByte4.java \ com/sun/crypto/provider/Cipher/AEAD/GCMIncrementDirect4.java \ sun/security/smartcardio/TestChannel.java \ + sun/security/smartcardio/TestCleaner.java \ sun/security/smartcardio/TestConnect.java \ sun/security/smartcardio/TestConnectAgain.java \ sun/security/smartcardio/TestControl.java \ diff --git a/test/jdk/sun/security/smartcardio/TestAll.java b/test/jdk/sun/security/smartcardio/TestAll.java index 72a0f85daaa..1911c58fbf0 100644 --- a/test/jdk/sun/security/smartcardio/TestAll.java +++ b/test/jdk/sun/security/smartcardio/TestAll.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -33,6 +33,7 @@ public class TestAll { private static final Class[] CLASSES = { TestDefault.class, TestChannel.class, + TestCleaner.class, TestConnect.class, TestConnectAgain.class, TestControl.class, diff --git a/test/jdk/sun/security/smartcardio/TestCleaner.java b/test/jdk/sun/security/smartcardio/TestCleaner.java new file mode 100644 index 00000000000..dfcecdb90f8 --- /dev/null +++ b/test/jdk/sun/security/smartcardio/TestCleaner.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8380391 + * @summary basic test of CardImpl Cleaner + * @modules java.base/java.lang.ref:open + * @modules java.smartcardio/javax.smartcardio + * @library /test/lib + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/manual/othervm + * -Xbootclasspath/a:. + * -XX:+UnlockDiagnosticVMOptions + * -XX:+WhiteBoxAPI + * TestCleaner + */ + +// This test requires special hardware; a card must be present + +import java.util.WeakHashMap; +import javax.smartcardio.Card; +import javax.smartcardio.CardTerminal; +import java.security.NoSuchAlgorithmException; + +import jdk.test.whitebox.WhiteBox; +import jtreg.SkippedException; + +/** + * Rudimentary test to confirm that the cleaning action does not prevent the + * CardImpl from becoming unreachable and being collected/cleaned. + */ +public class TestCleaner extends Utils { + static WhiteBox wb; + + public static void main(String[] args) throws Exception { + CardTerminal terminal = null; + try { + terminal = getTerminal(args); + } catch (NoSuchAlgorithmException e) { + if ("Error constructing TerminalFactory for PC/SC using SunPCSC".equals(e.getMessage())) { + // Cause is expected to be a PCSCException + if ("SCARD_E_NO_SERVICE".equals(e.getCause().getMessage())) { + throw new SkippedException("Skipping the test: " + + "Unable to construct TerminalFactory"); + } else { + throw e; + } + } + } + if (terminal == null) { + throw new SkippedException("Skipping the test: " + + "no card terminals available"); + } + + while (!terminal.isCardPresent()) { + System.out.println("*** Insert card!"); + Thread.sleep(1000); + } + + // Connect using any available protocol + Card card = terminal.connect("*"); + if (card == null) { + throw new SkippedException("Skipping the test: " + + "no card available"); + } + System.out.println("card is " + card); + + // Ensure card object can become unreachable + WeakHashMap whm = new WeakHashMap<>(); + whm.put(card, new Object()); + + System.out.println("Allow card object to be collected"); + card = null; + terminal = null; + + wb = WhiteBox.getWhiteBox(); + wb.fullGC(); + wb.waitForReferenceProcessing(); + + if (whm.size() > 0) { + throw new RuntimeException("*** TEST FAILED - Card could not be collected"); + } + System.out.println("Card object collected."); + } +}