Add overflow protections based on testing moshix' bbs
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 44s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 51s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m25s

This commit is contained in:
2026-10-14 15:53:31 +00:00
parent d8acd713af
commit 6e682d3b5c
11 changed files with 489 additions and 25 deletions
@@ -28,6 +28,9 @@ import java.util.logging.*;
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener { public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
private static final Logger log = Logger.getLogger(J3270App.class.getName()); private static final Logger log = Logger.getLogger(J3270App.class.getName());
private static final Logger PKG_LOG_ROOT = Logger.getLogger("haus.nightmare");
private static final Logger PKG_LOG_APP = Logger.getLogger("haus.nightmare.j3270");
private static final Logger PKG_LOG_LIB = Logger.getLogger("haus.nightmare.lib3270j");
// Active open windows tracking // Active open windows tracking
private static final java.util.List<J3270App> openWindows = new java.util.concurrent.CopyOnWriteArrayList<>(); private static final java.util.List<J3270App> openWindows = new java.util.concurrent.CopyOnWriteArrayList<>();
@@ -1462,9 +1465,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
consoleHandler.setFilter(appFilter); consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler); globalRoot.addHandler(consoleHandler);
Logger.getLogger("haus.nightmare").setLevel(Level.ALL); PKG_LOG_ROOT.setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL); PKG_LOG_APP.setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL); PKG_LOG_LIB.setLevel(Level.ALL);
try { try {
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) { java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
@@ -69,6 +69,10 @@ public class ConnectionConfig {
private int reconnectMaxRetries = 5; private int reconnectMaxRetries = 5;
private int tcpUserTimeoutMs = 0; private int tcpUserTimeoutMs = 0;
// Framing buffer sanity limits (RFC 885 / RFC 2355 EOR protection)
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 1048576; // 1MB (supports dense GDDM graphics, symbol sets, and DFT transfers)
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
// Host operating system type (TSO, CMS, CICS) // Host operating system type (TSO, CMS, CICS)
private String hostType = "TSO"; private String hostType = "TSO";
private boolean savedConnection = false; private boolean savedConnection = false;
@@ -549,6 +553,11 @@ public class ConnectionConfig {
public String getAssociatedPrinterLu() { return associatedPrinterLu; } public String getAssociatedPrinterLu() { return associatedPrinterLu; }
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; } public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
public int getMaxFramingBufferSize() { return maxFramingBufferSize; }
public void setMaxFramingBufferSize(int maxFramingBufferSize) {
this.maxFramingBufferSize = maxFramingBufferSize > 0 ? maxFramingBufferSize : DEFAULT_MAX_FRAMING_BUFFER_SIZE;
}
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() { public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port); haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
pcfg.setUseTls(useTls); pcfg.setUseTls(useTls);
@@ -559,6 +568,7 @@ public class ConnectionConfig {
pcfg.setPrinterLuName(associatedPrinterLu); pcfg.setPrinterLuName(associatedPrinterLu);
pcfg.setAssociatedDisplayLuName(luName); pcfg.setAssociatedDisplayLuName(luName);
pcfg.setCodePage(codePage); pcfg.setCodePage(codePage);
pcfg.setMaxFramingBufferSize(maxFramingBufferSize);
return pcfg; return pcfg;
} }
} }
@@ -17,21 +17,49 @@ import java.util.Arrays;
*/ */
public class FastByteBuffer extends OutputStream { public class FastByteBuffer extends OutputStream {
public static final int DEFAULT_INITIAL_CAPACITY = 32768;
public static final int DEFAULT_MAX_CAPACITY = 1048576; // 1MB (matches modern x3270 PDS_MAX for dense GDDM / GOCA vector graphics)
private final int initialCapacity;
private int maxCapacity;
private byte[] buf; private byte[] buf;
private int count; private int count;
public FastByteBuffer() { public FastByteBuffer() {
this(32768); this(DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_CAPACITY);
} }
public FastByteBuffer(int initialCapacity) { public FastByteBuffer(int initialCapacity) {
this.buf = new byte[Math.max(32, initialCapacity)]; this(initialCapacity, Math.max(initialCapacity, DEFAULT_MAX_CAPACITY));
}
public FastByteBuffer(int initialCapacity, int maxCapacity) {
this.initialCapacity = Math.max(32, initialCapacity);
this.maxCapacity = Math.max(this.initialCapacity, maxCapacity);
this.buf = new byte[this.initialCapacity];
this.count = 0; this.count = 0;
} }
public int getMaxCapacity() {
return maxCapacity;
}
public void setMaxCapacity(int maxCapacity) {
this.maxCapacity = Math.max(this.initialCapacity, maxCapacity);
}
public int getInitialCapacity() {
return initialCapacity;
}
private void ensureCapacity(int minCapacity) { private void ensureCapacity(int minCapacity) {
if (minCapacity > maxCapacity) {
throw new FramingBufferOverflowException(
"FastByteBuffer exceeded maximum capacity of " + maxCapacity + " bytes (requested: " + minCapacity + ")"
);
}
if (minCapacity > buf.length) { if (minCapacity > buf.length) {
int newCap = Math.max(buf.length << 1, minCapacity); int newCap = Math.min(maxCapacity, Math.max(buf.length << 1, minCapacity));
buf = Arrays.copyOf(buf, newCap); buf = Arrays.copyOf(buf, newCap);
} }
} }
@@ -64,6 +92,9 @@ public class FastByteBuffer extends OutputStream {
public void reset() { public void reset() {
count = 0; count = 0;
if (buf.length > maxCapacity) {
buf = new byte[maxCapacity];
}
} }
/** /**
@@ -0,0 +1,16 @@
package haus.nightmare.lib3270j.datastream;
/**
* Thrown when an inbound framing buffer exceeds its maximum configured capacity
* (typically 64KB) without encountering an end-of-record delimiter (IAC EOR).
*/
public class FramingBufferOverflowException extends RuntimeException {
public FramingBufferOverflowException(String message) {
super(message);
}
public FramingBufferOverflowException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -50,6 +50,7 @@ public class FTDft {
private int pendingByte = -1; private int pendingByte = -1;
// Record formatting buffers and SO/SI tracking // Record formatting buffers and SO/SI tracking
private static final int MAX_DFT_RECORD_BUFFER_SIZE = 131072; // 128KB sanity cap
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream(); private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
private boolean dbcsMode = false; private boolean dbcsMode = false;
private boolean hasPendingByteInDBCS = false; private boolean hasPendingByteInDBCS = false;
@@ -549,9 +550,17 @@ public class FTDft {
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl; int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
if (isRecfmV) { if (isRecfmV) {
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
downloadRecordBuffer.reset();
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
}
downloadRecordBuffer.write(data, offset, length); downloadRecordBuffer.write(data, offset, length);
processVariableDownloadRecords(config); processVariableDownloadRecords(config);
} else if (isRecfmF && lrecl > 0) { } else if (isRecfmF && lrecl > 0) {
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
downloadRecordBuffer.reset();
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
}
downloadRecordBuffer.write(data, offset, length); downloadRecordBuffer.write(data, offset, length);
processFixedDownloadRecords(lrecl, config); processFixedDownloadRecords(lrecl, config);
} else { } else {
@@ -38,6 +38,7 @@ public class NvtProcessor {
private static final int STATE_OSC = 4; private static final int STATE_OSC = 4;
private static final int STATE_OSC_ESC = 5; private static final int STATE_OSC_ESC = 5;
private static final int MAX_ESC_BUFFER_SIZE = 4096;
private int parseState = STATE_NORMAL; private int parseState = STATE_NORMAL;
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream(); private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
@@ -470,32 +471,51 @@ public class NvtProcessor {
} else if (parseState == STATE_OSC) { } else if (parseState == STATE_OSC) {
if (b == 0x07) { // BEL terminator if (b == 0x07) { // BEL terminator
handleOSC(escBuffer.toByteArray()); handleOSC(escBuffer.toByteArray());
escBuffer.reset();
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (b == 0x1B) { // Possible ESC \ (ST) } else if (b == 0x1B) { // Possible ESC \ (ST)
parseState = STATE_OSC_ESC; parseState = STATE_OSC_ESC;
} else if (b == 0x9C) { // 8-bit ST } else if (b == 0x9C) { // 8-bit ST
handleOSC(escBuffer.toByteArray()); handleOSC(escBuffer.toByteArray());
escBuffer.reset();
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else { } else {
escBuffer.write(b); if (escBuffer.size() < MAX_ESC_BUFFER_SIZE) {
escBuffer.write(b);
} else {
escBuffer.reset();
parseState = STATE_NORMAL;
}
} }
} else if (parseState == STATE_OSC_ESC) { } else if (parseState == STATE_OSC_ESC) {
if (b == '\\') { // ESC \ (ST) terminator if (b == '\\') { // ESC \ (ST) terminator
handleOSC(escBuffer.toByteArray()); handleOSC(escBuffer.toByteArray());
escBuffer.reset();
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else { } else {
// False alarm on ST, put ESC and byte into OSC buffer // False alarm on ST, put ESC and byte into OSC buffer
escBuffer.write(0x1B); if (escBuffer.size() + 2 <= MAX_ESC_BUFFER_SIZE) {
escBuffer.write(b); escBuffer.write(0x1B);
parseState = STATE_OSC; escBuffer.write(b);
parseState = STATE_OSC;
} else {
escBuffer.reset();
parseState = STATE_NORMAL;
}
} }
} else if (parseState == STATE_CSI) { } else if (parseState == STATE_CSI) {
escBuffer.write(b); if (escBuffer.size() < MAX_ESC_BUFFER_SIZE) {
escBuffer.write(b);
}
// CSI final bytes are in the range 0x40..0x7E // CSI final bytes are in the range 0x40..0x7E
if (b >= 0x40 && b <= 0x7E) { if (b >= 0x40 && b <= 0x7E) {
byte[] seq = escBuffer.toByteArray(); byte[] seq = escBuffer.toByteArray();
escBuffer.reset();
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols); curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
parseState = STATE_NORMAL; parseState = STATE_NORMAL;
} else if (escBuffer.size() >= MAX_ESC_BUFFER_SIZE) {
escBuffer.reset();
parseState = STATE_NORMAL;
} }
} }
} }
@@ -54,6 +54,8 @@ public class PrinterConfig {
private long autoFlushTimeoutMs = 2000; private long autoFlushTimeoutMs = 2000;
private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION; private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION;
private char graphicErrorReplacementChar = '?'; private char graphicErrorReplacementChar = '?';
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 1048576; // 1MB
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
public PrinterConfig() { public PrinterConfig() {
initDefaultChannelTable(); initDefaultChannelTable();
@@ -227,4 +229,9 @@ public class PrinterConfig {
public int[] getChannelTable() { public int[] getChannelTable() {
return channelTable.clone(); return channelTable.clone();
} }
public int getMaxFramingBufferSize() { return maxFramingBufferSize; }
public void setMaxFramingBufferSize(int maxFramingBufferSize) {
this.maxFramingBufferSize = maxFramingBufferSize > 0 ? maxFramingBufferSize : DEFAULT_MAX_FRAMING_BUFFER_SIZE;
}
} }
@@ -58,12 +58,17 @@ public class Telnet3270EP implements Runnable {
private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE; private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE;
// Buffer for Telnet streaming // Buffer for Telnet streaming
private final ByteArrayOutputStream recordBuffer = new ByteArrayOutputStream(); private static final int MAX_PRINTER_SUBNEG_SIZE = 8192;
private final int maxFramingBufferSize;
private boolean recordDiscardMode = false;
private final ByteArrayOutputStream recordBuffer;
private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream(); private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream();
private boolean inSubnegotiation = false; private boolean inSubnegotiation = false;
public Telnet3270EP(PrinterConfig config) { public Telnet3270EP(PrinterConfig config) {
this.config = config != null ? config : new PrinterConfig(); this.config = config != null ? config : new PrinterConfig();
this.maxFramingBufferSize = this.config.getMaxFramingBufferSize();
this.recordBuffer = new ByteArrayOutputStream(Math.min(32768, this.maxFramingBufferSize));
this.pd = new PD3270(this.config); this.pd = new PD3270(this.config);
this.translator = new EbcdicTranslator(this.config.getCodePage()); this.translator = new EbcdicTranslator(this.config.getCodePage());
this.scs = new PrintSCS3270(this.config, this.pd, this.translator); this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
@@ -73,6 +78,8 @@ public class Telnet3270EP implements Runnable {
public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) { public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
this.config = config != null ? config : new PrinterConfig(); this.config = config != null ? config : new PrinterConfig();
this.maxFramingBufferSize = this.config.getMaxFramingBufferSize();
this.recordBuffer = new ByteArrayOutputStream(Math.min(32768, this.maxFramingBufferSize));
this.pd = pd != null ? pd : new PD3270(this.config); this.pd = pd != null ? pd : new PD3270(this.config);
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage()); this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
this.scs = new PrintSCS3270(this.config, this.pd, this.translator); this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
@@ -191,6 +198,7 @@ public class Telnet3270EP implements Runnable {
this.recordBuffer.reset(); this.recordBuffer.reset();
this.subnegBuffer.reset(); this.subnegBuffer.reset();
this.inSubnegotiation = false; this.inSubnegotiation = false;
this.recordDiscardMode = false;
scs.resetSCSFormatDefaults(); scs.resetSCSFormatDefaults();
printPs.erasePrintBuffer(); printPs.erasePrintBuffer();
@@ -379,11 +387,19 @@ public class Telnet3270EP implements Runnable {
processSubnegotiation(subnegBuffer.toByteArray()); processSubnegotiation(subnegBuffer.toByteArray());
subnegBuffer.reset(); subnegBuffer.reset();
} else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) { } else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) {
subnegBuffer.write(TelnetConstants.IAC); if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
subnegBuffer.write(TelnetConstants.IAC);
}
idx++; idx++;
} }
} else { } else {
subnegBuffer.write(b); if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
subnegBuffer.write(b);
} else {
log.warning("Printer subnegotiation exceeded " + MAX_PRINTER_SUBNEG_SIZE + " bytes; aborting SB");
subnegBuffer.reset();
inSubnegotiation = false;
}
} }
continue; continue;
} }
@@ -394,15 +410,18 @@ public class Telnet3270EP implements Runnable {
switch (cmd) { switch (cmd) {
case TelnetConstants.IAC: case TelnetConstants.IAC:
recordBuffer.write(TelnetConstants.IAC); appendPrinterByte(TelnetConstants.IAC);
break; break;
case TelnetConstants.SB: case TelnetConstants.SB:
inSubnegotiation = true; inSubnegotiation = true;
subnegBuffer.reset(); subnegBuffer.reset();
break; break;
case TelnetConstants.EOR: case TelnetConstants.EOR:
processCompleteRecord(recordBuffer.toByteArray()); if (!recordDiscardMode) {
processCompleteRecord(recordBuffer.toByteArray());
}
recordBuffer.reset(); recordBuffer.reset();
recordDiscardMode = false;
break; break;
case TelnetConstants.DO: case TelnetConstants.DO:
if (idx < end) handleTelnetDo(data[idx++] & 0xFF); if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
@@ -420,11 +439,33 @@ public class Telnet3270EP implements Runnable {
break; break;
} }
} else { } else {
recordBuffer.write(b); appendPrinterByte(b);
} }
} }
} }
private void appendPrinterByte(int b) {
if (recordDiscardMode) {
return;
}
if (recordBuffer.size() >= maxFramingBufferSize) {
log.warning("Printer record buffer exceeded maximum capacity (" + maxFramingBufferSize +
" bytes) without IAC EOR; discarding until next EOR");
recordDiscardMode = true;
recordBuffer.reset();
return;
}
recordBuffer.write(b);
}
public boolean isRecordDiscardMode() {
return recordDiscardMode;
}
public int getMaxFramingBufferSize() {
return maxFramingBufferSize;
}
private void handleTelnetDo(int option) { private void handleTelnetDo(int option) {
try { try {
if (option == TelnetConstants.TELOPT_TN3270E) { if (option == TelnetConstants.TELOPT_TN3270E) {
@@ -46,8 +46,14 @@ public class TelnetFSM {
private final boolean[] myOpts = new boolean[256]; // options we have enabled private final boolean[] myOpts = new boolean[256]; // options we have enabled
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
// 3270 input buffer (accumulated between telnet framing) // 3270 input buffer (accumulated between telnet framing, bounded at maxFramingBufferSize)
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768); private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf;
// Discard mode if framing buffer overflows prior to receiving IAC EOR
private boolean recordDiscardMode = false;
// Sanity cap for Telnet subnegotiation parameters (RFC 1091, RFC 1572, RFC 2355)
private static final int MAX_SUBNEG_BUFFER_SIZE = 8192;
// Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C) // Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C)
private final byte[] nvtSingleByteScratch = new byte[1]; private final byte[] nvtSingleByteScratch = new byte[1];
@@ -165,6 +171,8 @@ public class TelnetFSM {
this.config = config; this.config = config;
this.screenBuffer = screenBuffer; this.screenBuffer = screenBuffer;
this.dsProcessor = dsProcessor; this.dsProcessor = dsProcessor;
int maxFraming = (config != null) ? config.getMaxFramingBufferSize() : ConnectionConfig.DEFAULT_MAX_FRAMING_BUFFER_SIZE;
this.ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(Math.min(32768, maxFraming), maxFraming);
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator()); this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
this.nvtProcessor.setOutputSender(this::sendNvtData); this.nvtProcessor.setOutputSender(this::sendNvtData);
} }
@@ -220,6 +228,7 @@ public class TelnetFSM {
deferredWillTtype = false; deferredWillTtype = false;
ttypeIndex = 0; ttypeIndex = 0;
luIndex = 0; luIndex = 0;
recordDiscardMode = false;
ibuf.reset(); ibuf.reset();
sbbuf.reset(); sbbuf.reset();
@@ -256,7 +265,7 @@ public class TelnetFSM {
if (isPlainNvt) { if (isPlainNvt) {
nvtProcessor.processNVTData(buf, start, i - start); nvtProcessor.processNVTData(buf, start, i - start);
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) { } else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
ibuf.write(buf, start, i - start); appendInboundData(buf, start, i - start);
} }
} }
if (i < end) { if (i < end) {
@@ -334,24 +343,74 @@ public class TelnetFSM {
// Accumulate data for 3270, TN3270E (including CONNECTED_E_NVT, SSCP-LU and unbound states, and pending) // Accumulate data for 3270, TN3270E (including CONNECTED_E_NVT, SSCP-LU and unbound states, and pending)
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) { if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
ibuf.write(c); appendInboundByte(c);
} }
} }
private void appendInboundByte(int c) {
if (recordDiscardMode) {
return;
}
if (ibuf.size() >= ibuf.getMaxCapacity()) {
handleFramingOverflow();
return;
}
try {
ibuf.write(c);
} catch (haus.nightmare.lib3270j.datastream.FramingBufferOverflowException e) {
handleFramingOverflow();
}
}
private void appendInboundData(byte[] buf, int offset, int len) {
if (recordDiscardMode || buf == null || len <= 0) {
return;
}
if (ibuf.size() + len > ibuf.getMaxCapacity()) {
handleFramingOverflow();
return;
}
try {
ibuf.write(buf, offset, len);
} catch (haus.nightmare.lib3270j.datastream.FramingBufferOverflowException e) {
handleFramingOverflow();
}
}
private void handleFramingOverflow() {
log.warning("Inbound 3270 framing buffer exceeded maximum capacity (" + ibuf.getMaxCapacity() +
" bytes) without IAC EOR; discarding record until next EOR");
recordDiscardMode = true;
ibuf.reset();
}
public boolean isRecordDiscardMode() {
return recordDiscardMode;
}
public int getMaxFramingBufferSize() {
return ibuf.getMaxCapacity();
}
public int getCurrentFramingBufferSize() {
return ibuf.size();
}
// ========== TNS_IAC ========== // ========== TNS_IAC ==========
private void processIAC(int c) { private void processIAC(int c) {
switch (c) { switch (c) {
case IAC: // Escaped IAC — literal 0xFF case IAC: // Escaped IAC — literal 0xFF
ibuf.write(c); appendInboundByte(c);
state = TNS_DATA; state = TNS_DATA;
break; break;
case EOR: // End of record — process accumulated 3270 data case EOR: // End of record — process accumulated 3270 data
log.fine("RCVD EOR"); log.fine("RCVD EOR");
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) { if (!recordDiscardMode && (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING)) {
processEndOfRecord(); processEndOfRecord();
} }
ibuf.reset(); ibuf.reset();
recordDiscardMode = false;
state = TNS_DATA; state = TNS_DATA;
break; break;
case WILL: state = TNS_WILL; break; case WILL: state = TNS_WILL; break;
@@ -527,7 +586,14 @@ public class TelnetFSM {
if (c == IAC) { if (c == IAC) {
state = TNS_SB_IAC; state = TNS_SB_IAC;
} else { } else {
sbbuf.write(c); if (sbbuf.size() < MAX_SUBNEG_BUFFER_SIZE) {
sbbuf.write(c);
} else {
log.warning("Telnet subnegotiation exceeded " + MAX_SUBNEG_BUFFER_SIZE +
" bytes without IAC SE; discarding and aborting SB");
sbbuf.reset();
state = TNS_DATA;
}
} }
} }
@@ -542,7 +608,9 @@ public class TelnetFSM {
state = TNS_DATA; state = TNS_DATA;
} else if (c == IAC) { } else if (c == IAC) {
// Escaped IAC within sub-negotiation // Escaped IAC within sub-negotiation
sbbuf.write(IAC); if (sbbuf.size() < MAX_SUBNEG_BUFFER_SIZE) {
sbbuf.write(IAC);
}
state = TNS_SB; state = TNS_SB;
} else { } else {
// Shouldn't happen, but recover // Shouldn't happen, but recover
@@ -1634,6 +1702,7 @@ public class TelnetFSM {
state = TNS_DATA; state = TNS_DATA;
java.util.Arrays.fill(myOpts, false); java.util.Arrays.fill(myOpts, false);
java.util.Arrays.fill(hisOpts, false); java.util.Arrays.fill(hisOpts, false);
recordDiscardMode = false;
ibuf.reset(); ibuf.reset();
sbbuf.reset(); sbbuf.reset();
tn3270eNegotiated = false; tn3270eNegotiated = false;
@@ -151,4 +151,39 @@ public class FastByteBufferTest {
executor.shutdown(); executor.shutdown();
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations"); assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
} }
@Test
@DisplayName("FastByteBuffer enforces maxCapacity and throws FramingBufferOverflowException")
public void testFastByteBufferMaxCapacityEnforcement() {
FastByteBuffer buf = new FastByteBuffer(64, 128);
assertEquals(128, buf.getMaxCapacity());
assertEquals(64, buf.getInitialCapacity());
byte[] chunk = new byte[64];
buf.write(chunk, 0, 64);
assertEquals(64, buf.size());
buf.write(chunk, 0, 64);
assertEquals(128, buf.size());
// Writing one more byte must throw FramingBufferOverflowException
assertThrows(FramingBufferOverflowException.class, () -> buf.write(0xFF));
// Writing a chunk that exceeds capacity must also throw
assertThrows(FramingBufferOverflowException.class, () -> buf.write(chunk, 0, 10));
// After reset, buffer is ready for use again
buf.reset();
assertEquals(0, buf.size());
buf.write(chunk, 0, 64);
assertEquals(64, buf.size());
}
@Test
@DisplayName("FastByteBuffer default maxCapacity is 1048576 bytes (1MB)")
public void testDefaultMaxCapacity() {
FastByteBuffer buf = new FastByteBuffer();
assertEquals(FastByteBuffer.DEFAULT_MAX_CAPACITY, buf.getMaxCapacity());
assertEquals(1048576, buf.getMaxCapacity());
}
} }
@@ -0,0 +1,223 @@
package haus.nightmare.lib3270j.telnet;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.datastream.DataStreamProcessor;
import haus.nightmare.lib3270j.nvt.NvtProcessor;
import haus.nightmare.lib3270j.printer.PrinterConfig;
import haus.nightmare.lib3270j.printer.Telnet3270EP;
import haus.nightmare.lib3270j.protocol.TelnetConstants;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
import static org.junit.jupiter.api.Assertions.*;
public class FramingBufferBoundedCapTest {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private TelnetFSM fsm;
private MockConnection connection;
private static class MockConnection extends TelnetConnection {
final List<byte[]> sentData = new ArrayList<>();
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
super(config, fsm);
}
@Override
public synchronized void sendRaw(byte[] data) {
sentData.add(data.clone());
}
@Override
public synchronized void sendRaw(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
sentData.add(b);
}
}
@BeforeEach
public void setup() {
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
haus.nightmare.lib3270j.input.InputProcessor inputProcessor =
new haus.nightmare.lib3270j.input.InputProcessor(screenBuffer, new EbcdicTranslator(), fsm);
dsProcessor.setInputProcessor(inputProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
private void feedBytes(int... bytes) {
byte[] b = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
b[i] = (byte) bytes[i];
}
fsm.feedBytes(b, 0, b.length);
}
@Test
@DisplayName("Framing buffer caps at 1MB and enters discard mode if stream lacks IAC EOR")
public void testFramingBufferOverflowDiscardsUntilEor() {
fsm.onConnected();
// Negotiate BINARY and EOR to establish standard 3270 streaming regime
feedBytes(IAC, DO, TELOPT_EOR);
feedBytes(IAC, WILL, TELOPT_EOR);
feedBytes(IAC, DO, TELOPT_BINARY);
feedBytes(IAC, WILL, TELOPT_BINARY);
assertEquals(1048576, fsm.getMaxFramingBufferSize(), "Default framing buffer cap should be 1MB (1048576 bytes)");
assertFalse(fsm.isRecordDiscardMode());
// Feed chunks totaling over 1MB without any IAC EOR delimiter
byte[] chunk = new byte[8192];
Arrays.fill(chunk, (byte) 0xC1); // EBCDIC 'A'
for (int i = 0; i < 128; i++) {
fsm.feedBytes(chunk, 0, chunk.length); // 128 * 8192 = 1,048,576 bytes
}
assertFalse(fsm.isRecordDiscardMode(), "Buffer should be at exact cap without overflowing yet");
// Now feed an additional chunk exceeding the 1MB cap without IAC EOR
fsm.feedBytes(chunk, 0, 100);
assertTrue(fsm.isRecordDiscardMode(), "FSM must enter recordDiscardMode when cap is exceeded without IAC EOR");
assertEquals(0, fsm.getCurrentFramingBufferSize(), "Buffer must be reset to free memory upon overflow");
// Further incoming data without IAC EOR must be discarded without growing the buffer
fsm.feedBytes(chunk, 0, chunk.length);
assertEquals(0, fsm.getCurrentFramingBufferSize(), "Discard mode must drop subsequent data without accumulating memory");
assertTrue(fsm.isRecordDiscardMode());
// Feeding IAC EOR must clear discard mode and reset buffer cleanly
feedBytes(IAC, EOR);
assertFalse(fsm.isRecordDiscardMode(), "IAC EOR must clear recordDiscardMode");
assertEquals(0, fsm.getCurrentFramingBufferSize());
// A subsequent valid record must be processed normally
// 0xF5 (Erase/Write), 0xC2 (WCC: unlock keyboard), 0x11 (SBA), 0x00, 0x00, 0xC1 ('A'), IAC, EOR
byte[] valid3270 = new byte[] { (byte) 0xF5, (byte) 0xC2, (byte) 0x11, 0x00, 0x00, (byte) 0xC1, (byte) IAC, (byte) EOR };
fsm.feedBytes(valid3270, 0, valid3270.length);
assertEquals('A', screenBuffer.getDisplayCell(0).ucs4);
}
@Test
@DisplayName("Custom maxFramingBufferSize on ConnectionConfig is respected by TelnetFSM")
public void testCustomMaxFramingBufferSize() {
ConnectionConfig customConfig = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
customConfig.setMaxFramingBufferSize(1024); // 1KB custom limit
TelnetFSM customFsm = new TelnetFSM(customConfig, screenBuffer, dsProcessor);
customFsm.setConnection(connection);
customFsm.onConnected();
assertEquals(1024, customFsm.getMaxFramingBufferSize());
// Negotiate BINARY and EOR
byte[] neg = new byte[] {
(byte) IAC, (byte) DO, (byte) TELOPT_EOR,
(byte) IAC, (byte) WILL, (byte) TELOPT_EOR,
(byte) IAC, (byte) DO, (byte) TELOPT_BINARY,
(byte) IAC, (byte) WILL, (byte) TELOPT_BINARY
};
customFsm.feedBytes(neg, 0, neg.length);
byte[] payload500 = new byte[500];
Arrays.fill(payload500, (byte) 0xC1);
customFsm.feedBytes(payload500, 0, 500);
assertEquals(500, customFsm.getCurrentFramingBufferSize());
assertFalse(customFsm.isRecordDiscardMode());
customFsm.feedBytes(payload500, 0, 500);
assertEquals(1000, customFsm.getCurrentFramingBufferSize());
assertFalse(customFsm.isRecordDiscardMode());
// Overflow over 1024 limit
customFsm.feedBytes(payload500, 0, 100);
assertTrue(customFsm.isRecordDiscardMode());
assertEquals(0, customFsm.getCurrentFramingBufferSize());
}
@Test
@DisplayName("Telnet subnegotiation buffer caps at 8KB if stream lacks IAC SE")
public void testSubnegotiationBufferCap() {
fsm.onConnected();
// Begin subnegotiation: IAC SB TELOPT_TTYPE
feedBytes(IAC, SB, TELOPT_TTYPE);
// Feed > 8192 bytes without IAC SE
byte[] largeSb = new byte[8200];
Arrays.fill(largeSb, (byte) 'X');
fsm.feedBytes(largeSb, 0, largeSb.length);
// Feed a regular byte: should be back in TNS_DATA, not accumulating in SB
feedBytes(IAC, NOP);
// Ensure no exception and session remains operational
assertTrue(true);
}
@Test
@DisplayName("Printer framing buffer caps at 1MB and enters discard mode without IAC EOR")
public void testPrinterFramingBufferCap() throws Exception {
PrinterConfig pcfg = new PrinterConfig("localhost", 23);
assertEquals(1048576, pcfg.getMaxFramingBufferSize());
Telnet3270EP printer = new Telnet3270EP(pcfg);
assertEquals(1048576, printer.getMaxFramingBufferSize());
assertFalse(printer.isRecordDiscardMode());
Method processIncoming = Telnet3270EP.class.getDeclaredMethod("processIncomingBytes", byte[].class, int.class, int.class);
processIncoming.setAccessible(true);
byte[] chunk = new byte[32768];
Arrays.fill(chunk, (byte) 0x40);
for (int i = 0; i < 33; i++) {
processIncoming.invoke(printer, chunk, 0, chunk.length); // 33 * 32768 = 1,081,344 bytes > 1,048,576
}
assertTrue(printer.isRecordDiscardMode(), "Printer must enter discard mode when record exceeds 1MB");
// Send IAC EOR
byte[] eor = new byte[] { (byte) IAC, (byte) EOR };
processIncoming.invoke(printer, eor, 0, eor.length);
assertFalse(printer.isRecordDiscardMode(), "Printer must clear discard mode on IAC EOR");
}
@Test
@DisplayName("NvtProcessor caps escape sequence buffer at 4KB if terminator is missing")
public void testNvtProcessorEscapeBufferCap() {
NvtProcessor nvt = fsm.getNvtProcessor();
// Stream ESC [ followed by 5000 digits without terminating byte (0x40..0x7E)
byte[] escStart = new byte[] { 0x1B, '[' };
nvt.processNVTData(escStart, 0, escStart.length);
byte[] digits = new byte[5000];
Arrays.fill(digits, (byte) '1');
nvt.processNVTData(digits, 0, digits.length);
// After escBuffer reaches 4096 bytes, it aborts escape parsing and returns to STATE_NORMAL.
// Subsequent digits are rendered as plain text in the screen buffer starting at cell 0.
assertEquals('1', screenBuffer.getCell(0).ucs4);
}
}