/*
* JBoss, Home of Professional Open Source.
* Copyright 2013, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.patching.runner;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import org.jboss.as.patching.logging.PatchLogger;
import org.wildfly.security.manager.WildFlySecurityManager;
import static org.jboss.as.patching.runner.PatchUtils.BACKUP_EXT;
import static org.jboss.as.patching.runner.PatchUtils.JAR_EXT;
/**
* Cripple a JAR or other zip file by flipping a bit in the end of central directory record
* This process can be reversed by flipping the bit back. Useful for rendering JARs non-executable.
*
* based on the org.jboss.as.server.deployment.scanner.ZipCompletionScanner
*
* @author David Jorm
* @author Emanuel Muckenhuber
*/
class PatchModuleInvalidationUtils {
private static final boolean ENABLE_INVALIDATION = Boolean.parseBoolean(WildFlySecurityManager.getPropertyPrivileged("org.wildfly.patching.jar.invalidation", "false"));
/**
* Local file header marker
*/
public static final long LOCSIG = 0x04034b50L;
/**
* Extra data descriptor marker
*/
public static final long EXTSIG = 0x08074b50L;
/**
* Central directory file header marker
*/
public static final long CENSIG = 0x02014b50L;
/**
* End of central directory record marker
*/
public static final int GOOD_ENDSIG = 0x06054b50; // Good signature
public static final int CRIPPLED_ENDSIG = 0x07054b50; // Crippled signature
/**
* Length of the fixed portion of a local file header
*/
public static final int LOCLEN = 30;
/**
* Length of the fixed portion of a central directory file header
*/
public static final int CENLEN = 46;
/**
* Length of the fixed portion of an End of central directory record
*/
public static final int ENDLEN = 22;
/**
* Position of the filename length in a local file header
*/
public static final int LOC_FILENAMELEN = 26;
/**
* Position of the extra field length in a local file header
*/
public static final int LOC_EXTFLDLEN = 28;
/**
* Position of the associated local file's compressed size in the central directory file header
*/
public static final int CENSIZ = 20;
/**
* Position of the associated local file's offset in the central directory file header
*/
public static final int CEN_LOC_OFFSET = 32;
/**
* Position of the 'start of central directory' field in an end of central directory record
*/
public static final int END_CENSTART = 16;
/**
* END_CENSTART value that indicates the zip is in ZIP 64 format
*/
public static final long ZIP64_MARKER = 0xFFFFFFFFL;
/**
* Position of the comment length in an end of central directory record
*/
public static final int END_COMMENTLEN = 20;
private static final int MAX_REVERSE_SCAN = (1 << 16) + ENDLEN;
private static final int CHUNK_SIZE = 4096;
private static final int ALPHABET_SIZE = 256;
private static final byte[] GOOD_ENDSIG_PATTERN = new byte[]{0x06, 0x05, 0x4b, 0x50}; // good signature
private static final byte[] CRIPPLED_ENDSIG_PATTERN = new byte[]{0x07, 0x05, 0x4b, 0x50}; // crippled signature
private static final int SIG_PATTERN_LENGTH = 4;
private static final int[] BAD_BYTE_SKIP = new int[ALPHABET_SIZE];
private static final byte[] LOCSIG_PATTERN = new byte[]{0x50, 0x4b, 0x03, 0x04};
private static final int[] LOC_BAD_BYTE_SKIP = new int[ALPHABET_SIZE];
static {
// Set up the Boyer Moore "bad character arrays" for our 3 patterns
// computeBadByteSkipArray(GOOD_ENDSIG_PATTERN, GOOD_END_BAD_BYTE_SKIP);
// computeBadByteSkipArray(CRIPPLED_ENDSIG_PATTERN, CRIPPLED_BAD_BYTE_SKIP);
computeBadByteSkipArray(GOOD_ENDSIG_PATTERN, CRIPPLED_ENDSIG_PATTERN, BAD_BYTE_SKIP);
computeBadByteSkipArray(LOCSIG_PATTERN, LOC_BAD_BYTE_SKIP);
}
/**
* Prevent instantiation
*/
private PatchModuleInvalidationUtils() {
}
/**
* Process a file.
*
* @param file the file to be processed
* @param mode the patching mode
* @throws IOException
*/
static void processFile(final IdentityPatchContext context, final File file, final PatchingTaskContext.Mode mode) throws IOException {
if (mode == PatchingTaskContext.Mode.APPLY) {
if (ENABLE_INVALIDATION) {
updateJar(file, GOOD_ENDSIG_PATTERN, BAD_BYTE_SKIP, CRIPPLED_ENDSIG, GOOD_ENDSIG);
backup(context, file);
}
} else if (mode == PatchingTaskContext.Mode.ROLLBACK) {
updateJar(file, CRIPPLED_ENDSIG_PATTERN, BAD_BYTE_SKIP, GOOD_ENDSIG, CRIPPLED_ENDSIG);
restore(context, file);
} else {
throw new IllegalStateException();
}
}
/**
* Update the central directory signature of a .jar.
*
* @param file the file to process
* @param searchPattern the search patter to use
* @param badSkipBytes the bad bytes skip table
* @param newSig the new signature
* @param endSig the expected signature
* @throws IOException
*/
private static void updateJar(final File file, final byte[] searchPattern, final int[] badSkipBytes, final int newSig, final int endSig) throws IOException {
final RandomAccessFile raf = new RandomAccessFile(file, "rw");
try {
final FileChannel channel = raf.getChannel();
try {
long pos = channel.size() - ENDLEN;
final ScanContext context;
if (newSig == CRIPPLED_ENDSIG) {
context = new ScanContext(GOOD_ENDSIG_PATTERN, CRIPPLED_ENDSIG_PATTERN);
} else if (newSig == GOOD_ENDSIG) {
context = new ScanContext(CRIPPLED_ENDSIG_PATTERN, GOOD_ENDSIG_PATTERN);
} else {
context = null;
}
if (!validateEndRecord(file, channel, pos, endSig)) {
pos = scanForEndSig(file, channel, context);
}
if (pos == -1) {
if (context.state == State.NOT_FOUND) {
// Don't fail patching if we cannot validate a valid zip
PatchLogger.ROOT_LOGGER.cannotInvalidateZip(file.getAbsolutePath());
}
return;
}
// Update the central directory record
channel.position(pos);
final ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(newSig);
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
} finally {
safeClose(channel);
}
} finally {
safeClose(raf);
}
}
private static void backup(final IdentityPatchContext context, final File file) {
String fileName = file.getName();
if (fileName.endsWith(JAR_EXT)) {
File targetFile = PatchUtils.getRenamedFileName(file);
if (!file.renameTo(targetFile)) {
if (context != null) {
context.failedToRenameFile(file, targetFile);
} else {
throw PatchLogger.ROOT_LOGGER.cannotRenameFileDuringBackup(file.getAbsolutePath());
}
}
}
}
private static void restore(final IdentityPatchContext context, final File file) {
String fileName = file.getName();
if (fileName.endsWith(BACKUP_EXT)) {
File targetFile = PatchUtils.getRenamedFileName(file);
if (!file.renameTo(targetFile)) {
if (context != null) {
context.failedToRenameFile(file, targetFile);
} else {
throw PatchLogger.ROOT_LOGGER.cannotRenameFileDuringRestore(file.getAbsolutePath());
}
}
}
}
/**
* Validates that the data structure at position startEndRecord has a field in the expected position
* that points to the start of the first central directory file, and, if so, that the file
* has a complete end of central directory record comment at the end.
*
* @param file the file being checked
* @param channel the channel
* @param startEndRecord the start of the end of central directory record
* @param endSig the end of central dir signature
* @return true if it can be confirmed that the end of directory record points to a central directory
* file and a complete comment is present, false otherwise
* @throws java.io.IOException
*/
private static boolean validateEndRecord(File file, FileChannel channel, long startEndRecord, long endSig) throws IOException {
try {
channel.position(startEndRecord);
final ByteBuffer endDirHeader = getByteBuffer(ENDLEN);
read(endDirHeader, channel);
if (endDirHeader.limit() < ENDLEN) {
// Couldn't read the full end of central directory record header
return false;
} else if (getUnsignedInt(endDirHeader, 0) != endSig) {
return false;
}
long pos = getUnsignedInt(endDirHeader, END_CENSTART);
// TODO deal with Zip64
if (pos == ZIP64_MARKER) {
return false;
}
ByteBuffer cdfhBuffer = getByteBuffer(CENLEN);
read(cdfhBuffer, channel, pos);
long header = getUnsignedInt(cdfhBuffer, 0);
if (header == CENSIG) {
long firstLoc = getUnsignedInt(cdfhBuffer, CEN_LOC_OFFSET);
long firstSize = getUnsignedInt(cdfhBuffer, CENSIZ);
if (firstLoc == 0) {
// normal case -- first bytes are the first local file
if (!validateLocalFileRecord(channel, 0, firstSize)) {
return false;
}
} else {
// confirm that firstLoc is indeed the first local file
long fileFirstLoc = scanForLocSig(channel);
if (firstLoc != fileFirstLoc) {
if (fileFirstLoc == 0) {
return false;
} else {
// scanForLocSig() found a LOCSIG, but not at position zero and not
// at the expected position.
// With a file like this, we can't tell if we're in a nested zip
// or we're in an outer zip and had the bad luck to find random bytes
// that look like LOCSIG.
return false;
}
}
}
// At this point, endDirHeader points to the correct end of central dir record.
// Just need to validate the record is complete, including any comment
int commentLen = getUnsignedShort(endDirHeader, END_COMMENTLEN);
long commentEnd = startEndRecord + ENDLEN + commentLen;
return commentEnd <= channel.size();
}
return false;
} catch (EOFException eof) {
// pos or firstLoc weren't really positions and moved us to an invalid location
return false;
}
}
/**
* Boyer Moore scan that proceeds backwards from the end of the file looking for endsig
*
* @param file the file being checked
* @param channel the channel
* @param context the scan context
* @return
* @throws IOException
*/
private static long scanForEndSig(final File file, final FileChannel channel, final ScanContext context) throws IOException {
// TODO Consider just reading in MAX_REVERSE_SCAN bytes -- increased peak memory cost but less complex
ByteBuffer bb = getByteBuffer(CHUNK_SIZE);
long start = channel.size();
long end = Math.max(0, start - MAX_REVERSE_SCAN);
long channelPos = Math.max(0, start - CHUNK_SIZE);
long lastChannelPos = channelPos;
while (lastChannelPos >= end) {
read(bb, channel, channelPos);
int actualRead = bb.limit();
int bufferPos = actualRead - 1;
while (bufferPos >= SIG_PATTERN_LENGTH) {
// Following is based on the Boyer Moore algorithm but simplified to reflect
// a) the pattern is static
// b) the pattern has no repeating bytes
int patternPos;
for (patternPos = SIG_PATTERN_LENGTH - 1;
patternPos >= 0 && context.matches(patternPos, bb.get(bufferPos - patternPos));
--patternPos) {
// empty loop while bytes match
}
// Switch gives same results as checking the "good suffix array" in the Boyer Moore algorithm
switch (patternPos) {
case -1: {
final State state = context.state;
// Pattern matched. Confirm is this is the start of a valid end of central dir record
long startEndRecord = channelPos + bufferPos - SIG_PATTERN_LENGTH + 1;
if (validateEndRecord(file, channel, startEndRecord, context.getSig())) {
if (state == State.FOUND) {
return startEndRecord;
} else {
return -1;
}
}
// wasn't a valid end record; continue scan
bufferPos -= 4;
break;
}
case 3: {
// No bytes matched; the common case.
// With our pattern, this is the only case where the Boyer Moore algorithm's "bad char array" may
// produce a shift greater than the "good suffix array" (which would shift 1 byte)
int idx = bb.get(bufferPos - patternPos) - Byte.MIN_VALUE;
bufferPos -= BAD_BYTE_SKIP[idx];
break;
}
default:
// 1 or more bytes matched
bufferPos -= 4;
}
}
// Move back a full chunk. If we didn't read a full chunk, that's ok,
// it means we read all data and the outer while loop will terminate
if (channelPos <= bufferPos) {
break;
}
lastChannelPos = channelPos;
channelPos -= Math.min(channelPos - bufferPos, CHUNK_SIZE - bufferPos);
}
return -1;
}
/**
* Boyer Moore scan that proceeds forwards from the end of the file looking for the first LOCSIG
*/
private static long scanForLocSig(FileChannel channel) throws IOException {
channel.position(0);
ByteBuffer bb = getByteBuffer(CHUNK_SIZE);
long end = channel.size();
while (channel.position() <= end) {
read(bb, channel);
int bufferPos = 0;
while (bufferPos <= bb.limit() - SIG_PATTERN_LENGTH) {
// Following is based on the Boyer Moore algorithm but simplified to reflect
// a) the size of the pattern is static
// b) the pattern is static and has no repeating bytes
int patternPos;
for (patternPos = SIG_PATTERN_LENGTH - 1;
patternPos >= 0 && LOCSIG_PATTERN[patternPos] == bb.get(bufferPos + patternPos);
--patternPos) {
// empty loop while bytes match
}
// Outer switch gives same results as checking the "good suffix array" in the Boyer Moore algorithm
switch (patternPos) {
case -1: {
// Pattern matched. Confirm is this is the start of a valid local file record
long startLocRecord = channel.position() - bb.limit() + bufferPos;
long currentPos = channel.position();
if (validateLocalFileRecord(channel, startLocRecord, -1)) {
return startLocRecord;
}
// Restore position in case it shifted
channel.position(currentPos);
// wasn't a valid local file record; continue scan
bufferPos += 4;
break;
}
case 3: {
// No bytes matched; the common case.
// With our pattern, this is the only case where the Boyer Moore algorithm's "bad char array" may
// produce a shift greater than the "good suffix array" (which would shift 1 byte)
int idx = bb.get(bufferPos + patternPos) - Byte.MIN_VALUE;
bufferPos += LOC_BAD_BYTE_SKIP[idx];
break;
}
default:
// 1 or more bytes matched
bufferPos += 4;
}
}
}
return -1;
}
/**
* Checks that the data starting at startLocRecord looks like a local file record header.
*
* @param channel the channel
* @param startLocRecord offset into channel of the start of the local record
* @param compressedSize expected compressed size of the file, or -1 to indicate this isn't known
*/
private static boolean validateLocalFileRecord(FileChannel channel, long startLocRecord, long compressedSize) throws IOException {
ByteBuffer lfhBuffer = getByteBuffer(LOCLEN);
read(lfhBuffer, channel, startLocRecord);
if (lfhBuffer.limit() < LOCLEN || getUnsignedInt(lfhBuffer, 0) != LOCSIG) {
return false;
}
if (compressedSize == -1) {
// We can't further evaluate
return true;
}
int fnLen = getUnsignedShort(lfhBuffer, LOC_FILENAMELEN);
int extFieldLen = getUnsignedShort(lfhBuffer, LOC_EXTFLDLEN);
long nextSigPos = startLocRecord + LOCLEN + compressedSize + fnLen + extFieldLen;
read(lfhBuffer, channel, nextSigPos);
long header = getUnsignedInt(lfhBuffer, 0);
return header == LOCSIG || header == EXTSIG || header == CENSIG;
}
private static ByteBuffer getByteBuffer(int capacity) {
ByteBuffer b = ByteBuffer.allocate(capacity);
b.order(ByteOrder.LITTLE_ENDIAN);
return b;
}
private static void read(ByteBuffer bb, FileChannel ch) throws IOException {
bb.clear();
ch.read(bb);
bb.flip();
}
private static void read(ByteBuffer bb, FileChannel ch, long pos) throws IOException {
bb.clear();
ch.read(bb, pos);
bb.flip();
}
private static long getUnsignedInt(ByteBuffer bb, int offset) {
return (bb.getInt(offset) & 0xffffffffL);
}
private static int getUnsignedShort(ByteBuffer bb, int offset) {
return (bb.getShort(offset) & 0xffff);
}
/**
* Fills the Boyer Moore "bad character array" for the given pattern
*/
private static void computeBadByteSkipArray(byte[] pattern, int[] badByteArray) {
for (int a = 0; a < ALPHABET_SIZE; a++) {
badByteArray[a] = pattern.length;
}
for (int j = 0; j < pattern.length - 1; j++) {
badByteArray[pattern[j] - Byte.MIN_VALUE] = pattern.length - j - 1;
}
}
private static void computeBadByteSkipArray(byte[] patterOne, byte[] patternTwo, int[] badByteArray) {
assert patterOne.length == patternTwo.length;
for (int a = 0; a < ALPHABET_SIZE; a++) {
badByteArray[a] = patterOne.length;
}
for (int j = 0; j < patternTwo.length - 1; j++) {
badByteArray[patterOne[j] - Byte.MIN_VALUE] = patterOne.length - j - 1;
}
for (int j = 0; j < patternTwo.length - 1; j++) {
badByteArray[patternTwo[j] - Byte.MIN_VALUE] = patternTwo.length - j - 1;
}
}
private static void safeClose(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception ignored) {
}
}
}
static enum State {
FOUND,
NOTHING_TODO,
NOT_FOUND
}
static class ScanContext {
private byte good;
private byte bad;
private byte[] pattern;
private State state = State.NOT_FOUND;
private ScanContext(byte[] good, byte[] bad) {
assert good.length== bad.length;
this.good = good[0];
this.bad = bad[0];
assert this.good != this.bad;
pattern = new byte[good.length];
for (int i = 1; i < good.length; i++) {
assert good[i] == bad[i];
pattern[i] = good[i];
}
}
boolean matches(int pos, byte b) {
if (pos == 0) {
if (b == good) {
state = State.FOUND;
return true;
} else if (b == bad) {
state = State.NOTHING_TODO;
return true;
}
return false;
} else {
return pattern[pos] == b;
}
}
int getSig() {
final byte b;
if (state == State.FOUND) {
b = good;
} else if (state == State.NOTHING_TODO) {
b = bad;
} else {
throw new IllegalStateException();
}
return ((b << 24) + (pattern[1] << 16) + (pattern[2] << 8) + (pattern[3] << 0));
}
}
}