diff options
author | Shawn Pearce <spearce@spearce.org> | 2016-07-04 18:04:43 -0700 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2017-02-11 00:20:36 +0100 |
commit | 0bff481d45db74db81a3b1b86f7401443a60d970 (patch) | |
tree | 47d11f395de3748b262c63e8d7d2e9f17d85f5e0 /org.eclipse.jgit | |
parent | 67da5635a47ad7680b3c60358a1fec080eef7d1e (diff) | |
download | jgit-0bff481d45db74db81a3b1b86f7401443a60d970.tar.gz jgit-0bff481d45db74db81a3b1b86f7401443a60d970.zip |
Limit receive commands
Place a configurable upper bound on the amount of command data
received from clients during `git push`. The limit is applied to the
encoded wire protocol format, not the JGit in-memory representation.
This allows clients to flexibly use the limit; shorter reference names
allow for more commands, longer reference names permit fewer commands
per batch.
Based on data gathered from many repositories at $DAY_JOB, the average
reference name is well under 200 bytes when encoded in UTF-8 (the wire
encoding). The new 3 MiB default receive.maxCommandBytes allows about
11,155 references in a single `git push` invocation. A Gerrit Code
Review system with six-digit change numbers could still encode 29,399
references in the 3 MiB maxCommandBytes limit.
Change-Id: I84317d396d25ab1b46820e43ae2b73943646032c
Signed-off-by: David Pursehouse <david.pursehouse@gmail.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Diffstat (limited to 'org.eclipse.jgit')
4 files changed, 132 insertions, 25 deletions
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 8bf2155d93..76d35b0a3b 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -609,6 +609,7 @@ tagOnRepoWithoutHEADCurrentlyNotSupported=Tag on repository without HEAD current theFactoryMustNotBeNull=The factory must not be null timeIsUncertain=Time is uncertain timerAlreadyTerminated=Timer already terminated +tooManyCommands=Too many commands tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)? topologicalSortRequired=Topological sort required. transactionAborted=transaction aborted diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 244356c13b..37499bbb3e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -669,6 +669,7 @@ public class JGitText extends TranslationBundle { /***/ public String theFactoryMustNotBeNull; /***/ public String timeIsUncertain; /***/ public String timerAlreadyTerminated; + /***/ public String tooManyCommands; /***/ public String tooManyIncludeRecursions; /***/ public String topologicalSortRequired; /***/ public String transportExceptionBadRef; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index 4d0803a339..b20a06864b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -97,6 +97,7 @@ import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PacketLineIn.InputOverLimitIOException; import org.eclipse.jgit.transport.ReceiveCommand.Result; import org.eclipse.jgit.util.io.InterruptTimer; import org.eclipse.jgit.util.io.LimitedInputStream; @@ -244,6 +245,8 @@ public abstract class BaseReceivePack { String userAgent; private Set<ObjectId> clientShallowCommits; private List<ReceiveCommand> commands; + private long maxCommandBytes; + private long maxDiscardBytes; private StringBuilder advertiseError; @@ -318,6 +321,8 @@ public abstract class BaseReceivePack { allowNonFastForwards = rc.allowNonFastForwards; allowOfsDelta = rc.allowOfsDelta; allowPushOptions = rc.allowPushOptions; + maxCommandBytes = rc.maxCommandBytes; + maxDiscardBytes = rc.maxDiscardBytes; advertiseRefsHook = AdvertiseRefsHook.DEFAULT; refFilter = RefFilter.DEFAULT; advertisedHaves = new HashSet<ObjectId>(); @@ -338,7 +343,8 @@ public abstract class BaseReceivePack { final boolean allowNonFastForwards; final boolean allowOfsDelta; final boolean allowPushOptions; - + final long maxCommandBytes; + final long maxDiscardBytes; final SignedPushConfig signedPush; ReceiveConfig(final Config config) { @@ -350,6 +356,12 @@ public abstract class BaseReceivePack { true); allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$ false); + maxCommandBytes = config.getLong("receive", //$NON-NLS-1$ + "maxCommandBytes", //$NON-NLS-1$ + 3 << 20); + maxDiscardBytes = config.getLong("receive", //$NON-NLS-1$ + "maxCommandDiscardBytes", //$NON-NLS-1$ + -1); signedPush = SignedPushConfig.KEY.parse(config); } } @@ -729,6 +741,38 @@ public abstract class BaseReceivePack { } /** + * Set the maximum number of command bytes to read from the client. + * + * @param limit + * command limit in bytes; if 0 there is no limit. + * @since 4.7 + */ + public void setMaxCommandBytes(long limit) { + maxCommandBytes = limit; + } + + /** + * Set the maximum number of command bytes to discard from the client. + * <p> + * Discarding remaining bytes allows this instance to consume the rest of + * the command block and send a human readable over-limit error via the + * side-band channel. If the client sends an excessive number of bytes this + * limit kicks in and the instance disconnects, resulting in a non-specific + * 'pipe closed', 'end of stream', or similar generic error at the client. + * <p> + * When the limit is set to {@code -1} the implementation will default to + * the larger of {@code 3 * maxCommandBytes} or {@code 3 MiB}. + * + * @param limit + * discard limit in bytes; if 0 there is no limit; if -1 the + * implementation tries to set a reasonable default. + * @since 4.7 + */ + public void setMaxCommandDiscardBytes(long limit) { + maxDiscardBytes = limit; + } + + /** * Set the maximum allowed Git object size. * <p> * If an object is larger than the given size the pack-parsing will throw an @@ -741,7 +785,6 @@ public abstract class BaseReceivePack { maxObjectSizeLimit = limit; } - /** * Set the maximum allowed pack size. * <p> @@ -1134,13 +1177,16 @@ public abstract class BaseReceivePack { * @throws IOException */ protected void recvCommands() throws IOException { + PacketLineIn pck = maxCommandBytes > 0 + ? new PacketLineIn(rawIn, maxCommandBytes) + : pckIn; PushCertificateParser certParser = getPushCertificateParser(); boolean firstPkt = true; try { for (;;) { String line; try { - line = pckIn.readString(); + line = pck.readString(); } catch (EOFException eof) { if (commands.isEmpty()) return; @@ -1163,13 +1209,13 @@ public abstract class BaseReceivePack { enableCapabilities(); if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) { - certParser.receiveHeader(pckIn, !isBiDirectionalPipe()); + certParser.receiveHeader(pck, !isBiDirectionalPipe()); continue; } } if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) { - certParser.receiveSignature(pckIn); + certParser.receiveSignature(pck); continue; } @@ -1186,18 +1232,31 @@ public abstract class BaseReceivePack { } pushCert = certParser.build(); if (hasCommands()) { - readPostCommands(pckIn); + readPostCommands(pck); } } catch (PackProtocolException e) { - if (sideBand) { - try { - pckIn.discardUntilEnd(); - } catch (IOException e2) { - // Ignore read failures attempting to discard. - } - } + discardCommands(); fatalError(e.getMessage()); throw e; + } catch (InputOverLimitIOException e) { + String msg = JGitText.get().tooManyCommands; + discardCommands(); + fatalError(msg); + throw new PackProtocolException(msg); + } + } + + private void discardCommands() { + if (sideBand) { + long max = maxDiscardBytes; + if (max < 0) { + max = Math.max(3 * maxCommandBytes, 3L << 20); + } + try { + new PacketLineIn(rawIn, max).discardUntilEnd(); + } catch (IOException e) { + // Ignore read failures attempting to discard. + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java index e142babd68..e70925693b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java @@ -87,19 +87,32 @@ public class PacketLineIn { ACK_READY; } + private final byte[] lineBuffer = new byte[SideBandOutputStream.SMALL_BUF]; private final InputStream in; + private long limit; - private final byte[] lineBuffer; + /** + * Create a new packet line reader. + * + * @param in + * the input stream to consume. + */ + public PacketLineIn(InputStream in) { + this(in, 0); + } /** * Create a new packet line reader. * - * @param i + * @param in * the input stream to consume. + * @param limit + * bytes to read from the input; unlimited if set to 0. + * @since 4.7 */ - public PacketLineIn(final InputStream i) { - in = i; - lineBuffer = new byte[SideBandOutputStream.SMALL_BUF]; + public PacketLineIn(InputStream in, long limit) { + this.in = in; + this.limit = limit; } AckNackResult readACK(final MutableObjectId returnedId) throws IOException { @@ -210,15 +223,48 @@ public class PacketLineIn { int readLength() throws IOException { IO.readFully(in, lineBuffer, 0, 4); + int len; try { - final int len = RawParseUtils.parseHexInt16(lineBuffer, 0); - if (len != 0 && len < 4) - throw new ArrayIndexOutOfBoundsException(); - return len; + len = RawParseUtils.parseHexInt16(lineBuffer, 0); } catch (ArrayIndexOutOfBoundsException err) { - throw new IOException(MessageFormat.format(JGitText.get().invalidPacketLineHeader, - "" + (char) lineBuffer[0] + (char) lineBuffer[1] //$NON-NLS-1$ - + (char) lineBuffer[2] + (char) lineBuffer[3])); + throw invalidHeader(); } + + if (len == 0) { + return 0; + } else if (len < 4) { + throw invalidHeader(); + } + + if (limit != 0) { + int n = len - 4; + if (limit < n) { + limit = -1; + try { + IO.skipFully(in, n); + } catch (IOException e) { + // Ignore failure discarding packet over limit. + } + throw new InputOverLimitIOException(); + } + // if set limit must not be 0 (means unlimited). + limit = n < limit ? limit - n : -1; + } + return len; + } + + private IOException invalidHeader() { + return new IOException(MessageFormat.format(JGitText.get().invalidPacketLineHeader, + "" + (char) lineBuffer[0] + (char) lineBuffer[1] //$NON-NLS-1$ + + (char) lineBuffer[2] + (char) lineBuffer[3])); + } + + /** + * IOException thrown by read when the configured input limit is exceeded. + * + * @since 4.7 + */ + public static class InputOverLimitIOException extends IOException { + private static final long serialVersionUID = 1L; } } |